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.
Files changed (29) hide show
  1. package/lib/index.js +204 -28
  2. package/lib/private/build-std-adapter-method.js +8 -4
  3. package/lib/private/machines/avg-records.js +1 -1
  4. package/lib/private/machines/begin-transaction.js +51 -0
  5. package/lib/private/machines/commit-transaction.js +50 -0
  6. package/lib/private/machines/count-records.js +1 -1
  7. package/lib/private/machines/create-each-record.js +1 -1
  8. package/lib/private/machines/create-record.js +2 -2
  9. package/lib/private/machines/define-physical-model.js +18 -9
  10. package/lib/private/machines/destroy-records.js +21 -8
  11. package/lib/private/machines/drop-physical-model.js +2 -2
  12. package/lib/private/machines/find-records.js +3 -3
  13. package/lib/private/machines/join.js +232 -66
  14. package/lib/private/machines/lease-connection.js +58 -0
  15. package/lib/private/machines/private/build-sqlite-where-clause.js +10 -8
  16. package/lib/private/machines/private/compile-statement.js +334 -0
  17. package/lib/private/machines/private/generate-join-sql-query.js +14 -6
  18. package/lib/private/machines/private/process-each-record.js +9 -2
  19. package/lib/private/machines/private/process-native-error.js +85 -40
  20. package/lib/private/machines/rollback-transaction.js +50 -0
  21. package/lib/private/machines/sum-records.js +1 -1
  22. package/lib/private/machines/update-records.js +27 -10
  23. package/package.json +8 -3
  24. package/tests/index.js +1 -1
  25. package/tests/runner.js +99 -0
  26. package/tests/transaction.test.js +562 -0
  27. package/tests/adapter.test.js +0 -534
  28. package/tests/datatypes.test.js +0 -293
  29. 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 ${sqlParts.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
- const columnName = model.attributes[key]
206
- ? model.attributes[key].columnName || key
207
- : key
208
- const fullColumnName = `${tableName}.${columnName}`
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 ${tableName}`
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
- for (const [attrName, attrDef] of Object.entries(WLModel.definition)) {
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
- // SQLite error codes
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
- err.footprint = {
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 (err.message.includes('UNIQUE constraint failed')) {
23
- err.footprint.identity = 'notUnique'
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
- if (match && match[1]) {
26
- err.footprint.keys.push(match[1])
27
- }
28
- }
29
- break
21
+ const keys = match && match[1] ? [match[1]] : []
30
22
 
31
- case SQLITE_BUSY:
32
- err.footprint = {
33
- identity: 'busy'
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
- case SQLITE_READONLY:
38
- err.footprint = {
39
- identity: 'readonly'
40
- }
41
- break
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 SQLITE_FULL:
44
- err.footprint = {
45
- identity: 'full'
46
- }
47
- break
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
- // Add more cases as needed for other error types
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, we might want to set a generic footprint
53
- err.footprint = {
54
- identity: 'error'
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(${numericFieldName}), 0) as total FROM ${tableName}`
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
- db.exec('BEGIN TRANSACTION')
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 = `SELECT ${pkColumnName} FROM ${tableName} WHERE ${sqliteWhere}`
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]) => `${column} = ?`)
97
+ .map(([column, value]) => `\`${column}\` = ?`)
91
98
  .join(', ')
92
- const updateSql = `UPDATE ${tableName} SET ${setClauses} WHERE ${sqliteWhere}`
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
- db.exec('ROLLBACK')
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
- db.exec('COMMIT')
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 ${tableName} WHERE ${pkColumnName} IN (${affectedIds.map(() => '?').join(', ')})`
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
- db.exec('COMMIT')
147
+ if (!wasInTransaction) {
148
+ db.exec('COMMIT')
149
+ }
135
150
  return exits.success(phRecords)
136
151
  } catch (err) {
137
- db.exec('ROLLBACK')
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)