sails-sqlite 0.1.0 → 0.2.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/lib/index.js +204 -28
- package/lib/private/build-std-adapter-method.js +8 -4
- package/lib/private/machines/avg-records.js +1 -1
- package/lib/private/machines/begin-transaction.js +51 -0
- package/lib/private/machines/commit-transaction.js +50 -0
- package/lib/private/machines/count-records.js +1 -1
- package/lib/private/machines/create-each-record.js +1 -1
- package/lib/private/machines/create-record.js +2 -2
- package/lib/private/machines/define-physical-model.js +18 -9
- package/lib/private/machines/destroy-records.js +21 -8
- package/lib/private/machines/drop-physical-model.js +2 -2
- package/lib/private/machines/find-records.js +3 -3
- package/lib/private/machines/join.js +232 -66
- package/lib/private/machines/lease-connection.js +58 -0
- package/lib/private/machines/private/build-sqlite-where-clause.js +10 -8
- package/lib/private/machines/private/compile-statement.js +334 -0
- package/lib/private/machines/private/generate-join-sql-query.js +14 -6
- package/lib/private/machines/private/process-each-record.js +9 -2
- package/lib/private/machines/private/process-native-error.js +85 -40
- package/lib/private/machines/rollback-transaction.js +50 -0
- package/lib/private/machines/sum-records.js +1 -1
- package/lib/private/machines/update-records.js +27 -10
- package/package.json +8 -3
- package/tests/index.js +1 -1
- package/tests/runner.js +99 -0
- package/tests/transaction.test.js +562 -0
- package/tests/adapter.test.js +0 -534
- package/tests/datatypes.test.js +0 -293
- package/tests/sequence.test.js +0 -153
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile a Waterline statement into a native SQLite query
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const buildSqliteWhereClause = require('./build-sqlite-where-clause')
|
|
6
|
+
|
|
7
|
+
module.exports = function compileStatement(statement) {
|
|
8
|
+
if (!statement) {
|
|
9
|
+
throw new Error('Statement is required')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let sql = ''
|
|
13
|
+
let bindings = []
|
|
14
|
+
|
|
15
|
+
// Handle UNION ALL queries
|
|
16
|
+
if (statement.unionAll && Array.isArray(statement.unionAll)) {
|
|
17
|
+
const unionQueries = []
|
|
18
|
+
let globalOrderBy = null
|
|
19
|
+
|
|
20
|
+
statement.unionAll.forEach((unionStatement) => {
|
|
21
|
+
let processedStatement = { ...unionStatement }
|
|
22
|
+
|
|
23
|
+
// Remove ORDER BY, LIMIT, SKIP from individual queries - apply globally
|
|
24
|
+
if (!globalOrderBy && unionStatement.orderBy) {
|
|
25
|
+
globalOrderBy = unionStatement.orderBy
|
|
26
|
+
}
|
|
27
|
+
delete processedStatement.orderBy
|
|
28
|
+
delete processedStatement.limit
|
|
29
|
+
delete processedStatement.skip
|
|
30
|
+
|
|
31
|
+
const compiledUnion = compileStatement(processedStatement)
|
|
32
|
+
unionQueries.push(compiledUnion.sql)
|
|
33
|
+
bindings = bindings.concat(compiledUnion.bindings || [])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
sql = unionQueries.join(' UNION ALL ')
|
|
37
|
+
|
|
38
|
+
// Apply global ORDER BY if present
|
|
39
|
+
if (
|
|
40
|
+
globalOrderBy &&
|
|
41
|
+
Array.isArray(globalOrderBy) &&
|
|
42
|
+
globalOrderBy.length > 0
|
|
43
|
+
) {
|
|
44
|
+
const orderClauses = globalOrderBy.map((orderItem) => {
|
|
45
|
+
if (typeof orderItem === 'string') {
|
|
46
|
+
return `\`${orderItem}\` ASC`
|
|
47
|
+
}
|
|
48
|
+
if (typeof orderItem === 'object') {
|
|
49
|
+
const key = Object.keys(orderItem)[0]
|
|
50
|
+
const direction =
|
|
51
|
+
orderItem[key].toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
|
|
52
|
+
return `\`${key}\` ${direction}`
|
|
53
|
+
}
|
|
54
|
+
return orderItem
|
|
55
|
+
})
|
|
56
|
+
sql += ` ORDER BY ${orderClauses.join(', ')}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { sql, bindings }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle regular SELECT statements
|
|
63
|
+
if (statement.select) {
|
|
64
|
+
// SELECT clause
|
|
65
|
+
if (Array.isArray(statement.select) && statement.select.length > 0) {
|
|
66
|
+
const selectColumns = statement.select.map((col) => {
|
|
67
|
+
// Handle columns with aliases (e.g., 'table.column as alias')
|
|
68
|
+
if (col.includes(' as ')) {
|
|
69
|
+
const [columnPart, aliasPart] = col.split(' as ')
|
|
70
|
+
const alias = aliasPart.trim()
|
|
71
|
+
|
|
72
|
+
// Process the column part
|
|
73
|
+
let formattedColumn
|
|
74
|
+
if (columnPart.includes('.')) {
|
|
75
|
+
const parts = columnPart.split('.')
|
|
76
|
+
if (parts.length === 2) {
|
|
77
|
+
const [tableName, columnName] = parts
|
|
78
|
+
formattedColumn = `\`${tableName}\`.\`${columnName}\``
|
|
79
|
+
} else {
|
|
80
|
+
formattedColumn = columnPart
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
formattedColumn = `\`${columnPart}\``
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return `${formattedColumn} AS ${alias}`
|
|
87
|
+
}
|
|
88
|
+
// Handle table-prefixed columns (e.g., 'tableName.columnName')
|
|
89
|
+
else if (col.includes('.')) {
|
|
90
|
+
const parts = col.split('.')
|
|
91
|
+
if (parts.length === 2) {
|
|
92
|
+
const [tableName, columnName] = parts
|
|
93
|
+
return `\`${tableName}\`.\`${columnName}\``
|
|
94
|
+
}
|
|
95
|
+
// Handle complex column expressions
|
|
96
|
+
return col
|
|
97
|
+
} else {
|
|
98
|
+
return `\`${col}\``
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
sql += `SELECT ${selectColumns.join(', ')}`
|
|
102
|
+
} else {
|
|
103
|
+
sql += 'SELECT *'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// FROM clause
|
|
107
|
+
if (statement.from) {
|
|
108
|
+
// Handle table aliases (e.g., "paymentTable as paymentTable__payments")
|
|
109
|
+
if (statement.from.includes(' as ')) {
|
|
110
|
+
const [tableName, alias] = statement.from.split(' as ')
|
|
111
|
+
sql += ` FROM \`${tableName.trim()}\` AS \`${alias.trim()}\``
|
|
112
|
+
} else {
|
|
113
|
+
sql += ` FROM \`${statement.from}\``
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// JOIN clauses
|
|
118
|
+
if (statement.leftOuterJoin && Array.isArray(statement.leftOuterJoin)) {
|
|
119
|
+
statement.leftOuterJoin.forEach((join) => {
|
|
120
|
+
if (join.from && join.on) {
|
|
121
|
+
// Handle table aliases in JOIN
|
|
122
|
+
let joinTable
|
|
123
|
+
if (join.from.includes(' as ')) {
|
|
124
|
+
const [tableName, alias] = join.from.split(' as ')
|
|
125
|
+
joinTable = `\`${tableName.trim()}\` AS \`${alias.trim()}\``
|
|
126
|
+
} else {
|
|
127
|
+
joinTable = `\`${join.from}\``
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
sql += ` LEFT OUTER JOIN ${joinTable} ON `
|
|
131
|
+
|
|
132
|
+
// Build the ON conditions
|
|
133
|
+
const onConditions = []
|
|
134
|
+
Object.keys(join.on).forEach((tableName) => {
|
|
135
|
+
const columnName = join.on[tableName]
|
|
136
|
+
// The key is a table name, value is a column name
|
|
137
|
+
// We need to format as table.column for both sides
|
|
138
|
+
const formattedTableCol = `\`${tableName}\`.\`${columnName}\``
|
|
139
|
+
onConditions.push(formattedTableCol)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Join conditions should be joined with =
|
|
143
|
+
// If we have 2 conditions, it should be table1.col1 = table2.col2
|
|
144
|
+
if (onConditions.length === 2) {
|
|
145
|
+
sql += `${onConditions[0]} = ${onConditions[1]}`
|
|
146
|
+
} else {
|
|
147
|
+
// Fallback for other cases
|
|
148
|
+
sql += onConditions.join(' AND ')
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// WHERE clause
|
|
155
|
+
if (statement.where) {
|
|
156
|
+
const whereResult = buildWhereClause(statement.where)
|
|
157
|
+
if (whereResult.clause) {
|
|
158
|
+
sql += ` WHERE ${whereResult.clause}`
|
|
159
|
+
bindings = bindings.concat(whereResult.bindings || [])
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ORDER BY clause
|
|
164
|
+
if (
|
|
165
|
+
statement.orderBy &&
|
|
166
|
+
Array.isArray(statement.orderBy) &&
|
|
167
|
+
statement.orderBy.length > 0
|
|
168
|
+
) {
|
|
169
|
+
const orderClauses = statement.orderBy.map((orderItem) => {
|
|
170
|
+
if (typeof orderItem === 'string') {
|
|
171
|
+
return `\`${orderItem}\` ASC`
|
|
172
|
+
}
|
|
173
|
+
if (typeof orderItem === 'object') {
|
|
174
|
+
const key = Object.keys(orderItem)[0]
|
|
175
|
+
const direction =
|
|
176
|
+
orderItem[key].toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
|
|
177
|
+
return `\`${key}\` ${direction}`
|
|
178
|
+
}
|
|
179
|
+
return orderItem
|
|
180
|
+
})
|
|
181
|
+
sql += ` ORDER BY ${orderClauses.join(', ')}`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// LIMIT clause
|
|
185
|
+
if (typeof statement.limit === 'number') {
|
|
186
|
+
sql += ` LIMIT ${statement.limit}`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// OFFSET clause
|
|
190
|
+
if (typeof statement.skip === 'number') {
|
|
191
|
+
sql += ` OFFSET ${statement.skip}`
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { sql, bindings }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build WHERE clause from Waterline criteria
|
|
200
|
+
*/
|
|
201
|
+
function buildWhereClause(whereObj) {
|
|
202
|
+
if (!whereObj || typeof whereObj !== 'object') {
|
|
203
|
+
return { clause: '', bindings: [] }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const conditions = []
|
|
207
|
+
const bindings = []
|
|
208
|
+
|
|
209
|
+
// Handle AND conditions
|
|
210
|
+
if (whereObj.and && Array.isArray(whereObj.and)) {
|
|
211
|
+
const andConditions = []
|
|
212
|
+
whereObj.and.forEach((condition) => {
|
|
213
|
+
const result = buildWhereClause(condition)
|
|
214
|
+
if (result.clause) {
|
|
215
|
+
andConditions.push(result.clause)
|
|
216
|
+
bindings.push(...result.bindings)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
if (andConditions.length > 0) {
|
|
220
|
+
conditions.push(`(${andConditions.join(' AND ')})`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle OR conditions
|
|
225
|
+
if (whereObj.or && Array.isArray(whereObj.or)) {
|
|
226
|
+
const orConditions = []
|
|
227
|
+
whereObj.or.forEach((condition) => {
|
|
228
|
+
const result = buildWhereClause(condition)
|
|
229
|
+
if (result.clause) {
|
|
230
|
+
orConditions.push(result.clause)
|
|
231
|
+
bindings.push(...result.bindings)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
if (orConditions.length > 0) {
|
|
235
|
+
conditions.push(`(${orConditions.join(' OR ')})`)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Handle field conditions
|
|
240
|
+
Object.keys(whereObj).forEach((key) => {
|
|
241
|
+
if (key === 'and' || key === 'or') {
|
|
242
|
+
return // Already handled above
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const value = whereObj[key]
|
|
246
|
+
let columnName
|
|
247
|
+
|
|
248
|
+
// Handle table.column format
|
|
249
|
+
if (key.includes('.')) {
|
|
250
|
+
const parts = key.split('.')
|
|
251
|
+
if (parts.length === 2) {
|
|
252
|
+
const [tableName, colName] = parts
|
|
253
|
+
columnName = `\`${tableName}\`.\`${colName}\``
|
|
254
|
+
} else {
|
|
255
|
+
columnName = key // fallback for complex expressions
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
columnName = `\`${key}\``
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (typeof value === 'object' && value !== null) {
|
|
262
|
+
// Handle operators
|
|
263
|
+
Object.keys(value).forEach((operator) => {
|
|
264
|
+
const operatorValue = value[operator]
|
|
265
|
+
|
|
266
|
+
switch (operator) {
|
|
267
|
+
case 'in':
|
|
268
|
+
if (Array.isArray(operatorValue) && operatorValue.length > 0) {
|
|
269
|
+
const placeholders = operatorValue.map(() => '?').join(', ')
|
|
270
|
+
conditions.push(`${columnName} IN (${placeholders})`)
|
|
271
|
+
bindings.push(...operatorValue)
|
|
272
|
+
}
|
|
273
|
+
break
|
|
274
|
+
case 'nin':
|
|
275
|
+
if (Array.isArray(operatorValue) && operatorValue.length > 0) {
|
|
276
|
+
const placeholders = operatorValue.map(() => '?').join(', ')
|
|
277
|
+
conditions.push(`${columnName} NOT IN (${placeholders})`)
|
|
278
|
+
bindings.push(...operatorValue)
|
|
279
|
+
}
|
|
280
|
+
break
|
|
281
|
+
case '>':
|
|
282
|
+
conditions.push(`${columnName} > ?`)
|
|
283
|
+
bindings.push(operatorValue)
|
|
284
|
+
break
|
|
285
|
+
case '>=':
|
|
286
|
+
conditions.push(`${columnName} >= ?`)
|
|
287
|
+
bindings.push(operatorValue)
|
|
288
|
+
break
|
|
289
|
+
case '<':
|
|
290
|
+
conditions.push(`${columnName} < ?`)
|
|
291
|
+
bindings.push(operatorValue)
|
|
292
|
+
break
|
|
293
|
+
case '<=':
|
|
294
|
+
conditions.push(`${columnName} <= ?`)
|
|
295
|
+
bindings.push(operatorValue)
|
|
296
|
+
break
|
|
297
|
+
case '!=':
|
|
298
|
+
case 'ne':
|
|
299
|
+
conditions.push(`${columnName} != ?`)
|
|
300
|
+
bindings.push(operatorValue)
|
|
301
|
+
break
|
|
302
|
+
case 'like':
|
|
303
|
+
conditions.push(`${columnName} LIKE ?`)
|
|
304
|
+
bindings.push(operatorValue)
|
|
305
|
+
break
|
|
306
|
+
case 'contains':
|
|
307
|
+
conditions.push(`${columnName} LIKE ?`)
|
|
308
|
+
bindings.push(`%${operatorValue}%`)
|
|
309
|
+
break
|
|
310
|
+
case 'startsWith':
|
|
311
|
+
conditions.push(`${columnName} LIKE ?`)
|
|
312
|
+
bindings.push(`${operatorValue}%`)
|
|
313
|
+
break
|
|
314
|
+
case 'endsWith':
|
|
315
|
+
conditions.push(`${columnName} LIKE ?`)
|
|
316
|
+
bindings.push(`%${operatorValue}`)
|
|
317
|
+
break
|
|
318
|
+
default:
|
|
319
|
+
conditions.push(`${columnName} = ?`)
|
|
320
|
+
bindings.push(operatorValue)
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
} else {
|
|
324
|
+
// Simple equality
|
|
325
|
+
conditions.push(`${columnName} = ?`)
|
|
326
|
+
bindings.push(value)
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
clause: conditions.join(' AND '),
|
|
332
|
+
bindings: bindings
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -130,7 +130,7 @@ module.exports = function generateJoinSqlQuery(joinCriteria, models, query) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Assemble the final SQL query
|
|
133
|
-
let sql = `SELECT ${sqlParts.select.join(', ')} FROM
|
|
133
|
+
let sql = `SELECT ${sqlParts.select.join(', ')} FROM \`${sqlParts.from}\``
|
|
134
134
|
|
|
135
135
|
if (sqlParts.joins.length > 0) {
|
|
136
136
|
sql += ' ' + sqlParts.joins.join(' ')
|
|
@@ -202,10 +202,18 @@ function buildWhereClause(whereObj, tableName, model) {
|
|
|
202
202
|
}
|
|
203
203
|
} else {
|
|
204
204
|
// Handle regular field conditions
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
let fullColumnName
|
|
206
|
+
if (key.includes('.')) {
|
|
207
|
+
// If the key already contains a table reference (e.g., "customerTable._id"),
|
|
208
|
+
// use it as-is instead of prepending the table name again
|
|
209
|
+
fullColumnName = key
|
|
210
|
+
} else {
|
|
211
|
+
// Otherwise, construct the full column name normally
|
|
212
|
+
const columnName = model.attributes[key]
|
|
213
|
+
? model.attributes[key].columnName || key
|
|
214
|
+
: key
|
|
215
|
+
fullColumnName = `${tableName}.${columnName}`
|
|
216
|
+
}
|
|
209
217
|
|
|
210
218
|
if (typeof value === 'object' && value !== null) {
|
|
211
219
|
// Handle operators like >, <, !=, in, etc.
|
|
@@ -301,7 +309,7 @@ function generateSqlQuery(joinCriteria, models) {
|
|
|
301
309
|
})
|
|
302
310
|
})
|
|
303
311
|
|
|
304
|
-
sql += ` FROM
|
|
312
|
+
sql += ` FROM \`${tableName}\``
|
|
305
313
|
|
|
306
314
|
// Add join clauses
|
|
307
315
|
joins.forEach((join, index) => {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* to the format expected by Waterline. This follows performance best practices.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const _ = require('@sailshq/lodash')
|
|
8
9
|
const { eachRecordDeep } = require('waterline-utils')
|
|
9
10
|
|
|
10
11
|
module.exports = function processEachRecord(options) {
|
|
@@ -45,7 +46,13 @@ module.exports = function processEachRecord(options) {
|
|
|
45
46
|
eachRecordDeep(
|
|
46
47
|
records,
|
|
47
48
|
(record, WLModel) => {
|
|
48
|
-
|
|
49
|
+
// Guard against null/undefined WLModel or definition
|
|
50
|
+
if (!WLModel || !WLModel.definition) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Use _.each instead of Object.entries for compatibility
|
|
55
|
+
_.each(WLModel.definition, (attrDef, attrName) => {
|
|
49
56
|
const columnName = attrDef.columnName || attrName
|
|
50
57
|
|
|
51
58
|
if (columnName in record) {
|
|
@@ -90,7 +97,7 @@ module.exports = function processEachRecord(options) {
|
|
|
90
97
|
// Add more type conversions as needed
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
|
-
}
|
|
100
|
+
})
|
|
94
101
|
},
|
|
95
102
|
true,
|
|
96
103
|
identity,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const flaverr = require('flaverr')
|
|
2
|
+
|
|
1
3
|
module.exports = function processNativeError(err) {
|
|
2
4
|
if (err.footprint !== undefined) {
|
|
3
5
|
return new Error(
|
|
@@ -5,55 +7,98 @@ module.exports = function processNativeError(err) {
|
|
|
5
7
|
)
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
//
|
|
9
|
-
const SQLITE_CONSTRAINT = 19
|
|
10
|
-
const SQLITE_BUSY = 5
|
|
11
|
-
const SQLITE_READONLY = 8
|
|
12
|
-
const SQLITE_FULL = 13
|
|
13
|
-
|
|
10
|
+
// better-sqlite3 uses string-based error codes
|
|
14
11
|
switch (err.code) {
|
|
15
|
-
case SQLITE_CONSTRAINT:
|
|
16
|
-
|
|
17
|
-
identity: 'violation',
|
|
18
|
-
keys: []
|
|
19
|
-
}
|
|
20
|
-
|
|
12
|
+
case 'SQLITE_CONSTRAINT':
|
|
13
|
+
case 'SQLITE_CONSTRAINT_UNIQUE':
|
|
21
14
|
// Check if it's a UNIQUE constraint violation
|
|
22
|
-
if (
|
|
23
|
-
err.
|
|
15
|
+
if (
|
|
16
|
+
err.code === 'SQLITE_CONSTRAINT_UNIQUE' ||
|
|
17
|
+
err.message.includes('UNIQUE constraint failed')
|
|
18
|
+
) {
|
|
19
|
+
// Extract the column name from the error message
|
|
24
20
|
const match = err.message.match(/UNIQUE constraint failed: \w+\.(\w+)/)
|
|
25
|
-
|
|
26
|
-
err.footprint.keys.push(match[1])
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
break
|
|
21
|
+
const keys = match && match[1] ? [match[1]] : []
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
return flaverr(
|
|
24
|
+
{
|
|
25
|
+
name: 'UsageError',
|
|
26
|
+
code: 'E_UNIQUE',
|
|
27
|
+
message: err.message,
|
|
28
|
+
footprint: {
|
|
29
|
+
identity: 'notUnique',
|
|
30
|
+
keys: keys
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
err
|
|
34
|
+
)
|
|
34
35
|
}
|
|
35
|
-
break
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
// Generic constraint violation
|
|
38
|
+
return flaverr(
|
|
39
|
+
{
|
|
40
|
+
name: 'UsageError',
|
|
41
|
+
code: 'E_CONSTRAINT',
|
|
42
|
+
message: err.message,
|
|
43
|
+
footprint: {
|
|
44
|
+
identity: 'violation',
|
|
45
|
+
keys: []
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
err
|
|
49
|
+
)
|
|
42
50
|
|
|
43
|
-
case
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
case 'SQLITE_BUSY':
|
|
52
|
+
return flaverr(
|
|
53
|
+
{
|
|
54
|
+
name: 'UsageError',
|
|
55
|
+
code: 'E_BUSY',
|
|
56
|
+
message: err.message,
|
|
57
|
+
footprint: {
|
|
58
|
+
identity: 'busy'
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
err
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
case 'SQLITE_READONLY':
|
|
65
|
+
return flaverr(
|
|
66
|
+
{
|
|
67
|
+
name: 'UsageError',
|
|
68
|
+
code: 'E_READONLY',
|
|
69
|
+
message: err.message,
|
|
70
|
+
footprint: {
|
|
71
|
+
identity: 'readonly'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
err
|
|
75
|
+
)
|
|
48
76
|
|
|
49
|
-
|
|
77
|
+
case 'SQLITE_FULL':
|
|
78
|
+
return flaverr(
|
|
79
|
+
{
|
|
80
|
+
name: 'UsageError',
|
|
81
|
+
code: 'E_FULL',
|
|
82
|
+
message: err.message,
|
|
83
|
+
footprint: {
|
|
84
|
+
identity: 'full'
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
err
|
|
88
|
+
)
|
|
50
89
|
|
|
51
90
|
default:
|
|
52
|
-
// For unhandled errors,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
// For unhandled errors, return a generic error with footprint
|
|
92
|
+
return flaverr(
|
|
93
|
+
{
|
|
94
|
+
name: 'Error',
|
|
95
|
+
code: 'E_UNKNOWN',
|
|
96
|
+
message: err.message,
|
|
97
|
+
footprint: {
|
|
98
|
+
identity: 'catchall'
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
err
|
|
102
|
+
)
|
|
56
103
|
}
|
|
57
|
-
|
|
58
|
-
return err
|
|
59
104
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module dependencies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Rollback Transaction
|
|
7
|
+
*
|
|
8
|
+
* Rollback the current database transaction on the provided connection.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
friendlyName: 'Rollback transaction',
|
|
13
|
+
|
|
14
|
+
description:
|
|
15
|
+
'Rollback the current database transaction on the provided connection.',
|
|
16
|
+
|
|
17
|
+
moreInfoUrl:
|
|
18
|
+
'https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#transactionfunction---function',
|
|
19
|
+
|
|
20
|
+
inputs: {
|
|
21
|
+
connection: {
|
|
22
|
+
description:
|
|
23
|
+
'An active database connection that was acquired from a manager.',
|
|
24
|
+
example: '===',
|
|
25
|
+
required: true
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
meta: {
|
|
29
|
+
description: 'Additional options for this query.',
|
|
30
|
+
example: '==='
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
fn: function rollbackTransaction(inputs, exits) {
|
|
35
|
+
const db = inputs.connection
|
|
36
|
+
const meta = inputs.meta || {}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (!db.inTransaction) {
|
|
40
|
+
return exits.error(new Error('No active transaction to rollback.'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
db.prepare('ROLLBACK TRANSACTION').run()
|
|
44
|
+
|
|
45
|
+
return exits.success()
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return exits.error(err)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -59,7 +59,7 @@ module.exports = {
|
|
|
59
59
|
const db = inputs.connection
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
|
-
let sumQuery = `SELECT COALESCE(SUM(
|
|
62
|
+
let sumQuery = `SELECT COALESCE(SUM(\`${numericFieldName}\`), 0) as total FROM \`${tableName}\``
|
|
63
63
|
if (whereClause) {
|
|
64
64
|
sumQuery += ` WHERE ${whereClause}`
|
|
65
65
|
}
|
|
@@ -72,24 +72,33 @@ module.exports = {
|
|
|
72
72
|
|
|
73
73
|
const db = inputs.connection
|
|
74
74
|
|
|
75
|
+
// Check if we're already in a transaction
|
|
76
|
+
const wasInTransaction = db.inTransaction
|
|
77
|
+
|
|
75
78
|
try {
|
|
76
|
-
// Start a transaction
|
|
77
|
-
|
|
79
|
+
// Start a transaction only if we're not already in one
|
|
80
|
+
if (!wasInTransaction) {
|
|
81
|
+
db.exec('BEGIN TRANSACTION')
|
|
82
|
+
}
|
|
78
83
|
|
|
79
84
|
let affectedIds = []
|
|
80
85
|
|
|
81
86
|
if (isFetchEnabled) {
|
|
82
87
|
// Get the IDs of records which match this criteria
|
|
83
|
-
const selectSql =
|
|
88
|
+
const selectSql = sqliteWhere
|
|
89
|
+
? `SELECT \`${pkColumnName}\` FROM \`${tableName}\` WHERE ${sqliteWhere}`
|
|
90
|
+
: `SELECT \`${pkColumnName}\` FROM \`${tableName}\``
|
|
84
91
|
const selectStmt = db.prepare(selectSql)
|
|
85
92
|
affectedIds = selectStmt.all().map((row) => row[pkColumnName])
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
// Prepare the UPDATE statement
|
|
89
96
|
const setClauses = Object.entries(s3q.valuesToSet)
|
|
90
|
-
.map(([column, value]) =>
|
|
97
|
+
.map(([column, value]) => `\`${column}\` = ?`)
|
|
91
98
|
.join(', ')
|
|
92
|
-
const updateSql =
|
|
99
|
+
const updateSql = sqliteWhere
|
|
100
|
+
? `UPDATE \`${tableName}\` SET ${setClauses} WHERE ${sqliteWhere}`
|
|
101
|
+
: `UPDATE \`${tableName}\` SET ${setClauses}`
|
|
93
102
|
const updateStmt = db.prepare(updateSql)
|
|
94
103
|
|
|
95
104
|
// Execute the UPDATE
|
|
@@ -107,7 +116,9 @@ module.exports = {
|
|
|
107
116
|
s3q.valuesToSet[pkColumnName] !== undefined &&
|
|
108
117
|
affectedIds.length > 1
|
|
109
118
|
) {
|
|
110
|
-
|
|
119
|
+
if (!wasInTransaction) {
|
|
120
|
+
db.exec('ROLLBACK')
|
|
121
|
+
}
|
|
111
122
|
return exits.error(
|
|
112
123
|
new Error(
|
|
113
124
|
'Consistency violation: Updated multiple records to have the same primary key value. (PK values should be unique!)'
|
|
@@ -117,12 +128,14 @@ module.exports = {
|
|
|
117
128
|
|
|
118
129
|
// If fetch is not enabled, we're done
|
|
119
130
|
if (!isFetchEnabled) {
|
|
120
|
-
|
|
131
|
+
if (!wasInTransaction) {
|
|
132
|
+
db.exec('COMMIT')
|
|
133
|
+
}
|
|
121
134
|
return exits.success()
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
// Fetch the updated records
|
|
125
|
-
const fetchSql = `SELECT * FROM
|
|
138
|
+
const fetchSql = `SELECT * FROM \`${tableName}\` WHERE \`${pkColumnName}\` IN (${affectedIds.map(() => '?').join(', ')})`
|
|
126
139
|
const fetchStmt = db.prepare(fetchSql)
|
|
127
140
|
const phRecords = fetchStmt.all(affectedIds)
|
|
128
141
|
|
|
@@ -131,10 +144,14 @@ module.exports = {
|
|
|
131
144
|
processNativeRecord(phRecord, WLModel, s3q.meta)
|
|
132
145
|
})
|
|
133
146
|
|
|
134
|
-
|
|
147
|
+
if (!wasInTransaction) {
|
|
148
|
+
db.exec('COMMIT')
|
|
149
|
+
}
|
|
135
150
|
return exits.success(phRecords)
|
|
136
151
|
} catch (err) {
|
|
137
|
-
|
|
152
|
+
if (!wasInTransaction) {
|
|
153
|
+
db.exec('ROLLBACK')
|
|
154
|
+
}
|
|
138
155
|
err = processNativeError(err)
|
|
139
156
|
if (err.footprint && err.footprint.identity === 'notUnique') {
|
|
140
157
|
return exits.notUnique(err)
|