sails-sqlite 0.2.2 → 0.2.4

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 CHANGED
@@ -92,7 +92,7 @@ module.exports = {
92
92
  // Default configuration for connections
93
93
  defaults: {
94
94
  schema: false,
95
- url: 'db/local.db',
95
+ url: 'db/local.sqlite',
96
96
  pragmas: {
97
97
  journal_mode: 'WAL'
98
98
  }
@@ -59,12 +59,12 @@ module.exports = {
59
59
 
60
60
  try {
61
61
  let avgQuery = `SELECT COALESCE(AVG(\`${numericFieldName}\`), 0) as average FROM \`${tableName}\``
62
- if (whereClause) {
63
- avgQuery += ` WHERE ${whereClause}`
62
+ if (whereClause.clause) {
63
+ avgQuery += ` WHERE ${whereClause.clause}`
64
64
  }
65
65
 
66
66
  const stmt = db.prepare(avgQuery)
67
- const result = stmt.get()
67
+ const result = stmt.get(...whereClause.bindings)
68
68
 
69
69
  return exits.success(result.average)
70
70
  } catch (err) {
@@ -63,12 +63,12 @@ module.exports = {
63
63
 
64
64
  try {
65
65
  let countQuery = `SELECT COUNT(*) as count FROM \`${tableName}\``
66
- if (whereClause) {
67
- countQuery += ` WHERE ${whereClause}`
66
+ if (whereClause.clause) {
67
+ countQuery += ` WHERE ${whereClause.clause}`
68
68
  }
69
69
 
70
70
  const stmt = db.prepare(countQuery)
71
- const result = stmt.get()
71
+ const result = stmt.get(...whereClause.bindings)
72
72
 
73
73
  return exits.success(result.count)
74
74
  } catch (err) {
@@ -54,7 +54,7 @@ module.exports = {
54
54
  const pkColumnName = WLModel.attributes[WLModel.primaryKey].columnName
55
55
  const isFetchEnabled = !!(s3q.meta && s3q.meta.fetch)
56
56
 
57
- const sqliteWhere = buildSqliteWhereClause(
57
+ const whereClause = buildSqliteWhereClause(
58
58
  s3q.criteria.where,
59
59
  WLModel,
60
60
  s3q.meta
@@ -68,29 +68,29 @@ module.exports = {
68
68
  try {
69
69
  // Start a transaction only if we're not already in one
70
70
  if (!wasInTransaction) {
71
- db.exec('BEGIN TRANSACTION')
71
+ db.prepare('BEGIN TRANSACTION').run()
72
72
  }
73
73
 
74
74
  let phRecords
75
75
  if (isFetchEnabled) {
76
76
  // Fetch matching records before deletion
77
- const selectSql = sqliteWhere
78
- ? `SELECT * FROM \`${tableName}\` WHERE ${sqliteWhere}`
77
+ const selectSql = whereClause.clause
78
+ ? `SELECT * FROM \`${tableName}\` WHERE ${whereClause.clause}`
79
79
  : `SELECT * FROM \`${tableName}\``
80
80
  const selectStmt = db.prepare(selectSql)
81
- phRecords = selectStmt.all()
81
+ phRecords = selectStmt.all(...whereClause.bindings)
82
82
  }
83
83
 
84
84
  // Perform the deletion
85
- const deleteSql = sqliteWhere
86
- ? `DELETE FROM \`${tableName}\` WHERE ${sqliteWhere}`
85
+ const deleteSql = whereClause.clause
86
+ ? `DELETE FROM \`${tableName}\` WHERE ${whereClause.clause}`
87
87
  : `DELETE FROM \`${tableName}\``
88
88
  const deleteStmt = db.prepare(deleteSql)
89
- const deleteInfo = deleteStmt.run()
89
+ const deleteInfo = deleteStmt.run(...whereClause.bindings)
90
90
 
91
91
  // Commit the transaction only if we started it
92
92
  if (!wasInTransaction) {
93
- db.exec('COMMIT')
93
+ db.prepare('COMMIT').run()
94
94
  }
95
95
 
96
96
  if (!isFetchEnabled) {
@@ -106,7 +106,7 @@ module.exports = {
106
106
  } catch (err) {
107
107
  // Rollback the transaction in case of error (only if we started it)
108
108
  if (!wasInTransaction) {
109
- db.exec('ROLLBACK')
109
+ db.prepare('ROLLBACK').run()
110
110
  }
111
111
  return exits.error(err)
112
112
  }
@@ -70,8 +70,8 @@ module.exports = {
70
70
  WLModel,
71
71
  s3q.meta
72
72
  )
73
- if (whereClause) {
74
- sqlQuery += ` WHERE ${whereClause}`
73
+ if (whereClause.clause) {
74
+ sqlQuery += ` WHERE ${whereClause.clause}`
75
75
  }
76
76
 
77
77
  // Handle SORT clause
@@ -99,12 +99,9 @@ module.exports = {
99
99
  sqlQuery += ` OFFSET ${s3q.criteria.skip}`
100
100
  }
101
101
 
102
- // Use prepared statement caching for better performance
103
- const stmt = db.getPreparedStatement
104
- ? db.getPreparedStatement(sqlQuery)
105
- : db.prepare(sqlQuery)
106
-
107
- const nativeResult = stmt.all()
102
+ // Use prepared statement with bindings for security
103
+ const stmt = db.prepare(sqlQuery)
104
+ const nativeResult = stmt.all(...whereClause.bindings)
108
105
 
109
106
  // Process records
110
107
  const phRecords = nativeResult.map((record) => {
@@ -2,90 +2,147 @@
2
2
  * buildSqliteWhereClause()
3
3
  *
4
4
  * Build a SQLite WHERE clause from the specified S3Q `where` clause.
5
+ * Uses parameterized queries for security.
5
6
  * > Note: The provided `where` clause is NOT mutated.
6
7
  *
7
8
  * @param {Object} whereClause [`where` clause from the criteria of a S3Q]
8
9
  * @param {Object} WLModel
9
10
  * @param {Object?} meta [`meta` query key from the s3q]
10
11
  *
11
- * @returns {String} [SQLite WHERE clause]
12
+ * @returns {Object} [{ clause: String, bindings: Array }]
12
13
  */
13
14
  module.exports = function buildSqliteWhereClause(whereClause, WLModel, meta) {
14
15
  // Handle null, undefined, or empty `where` clause.
15
16
  if (!whereClause || Object.keys(whereClause).length === 0) {
16
- return ''
17
+ return { clause: '', bindings: [] }
17
18
  }
18
19
 
19
- // Recursively build and return a transformed `where` clause for use with SQLite.
20
- function recurse(branch, isRoot = true) {
21
- const clauses = []
22
- for (const [key, value] of Object.entries(branch)) {
23
- if (key === 'and' || key === 'or') {
24
- const subClauses = value.map((subBranch) => recurse(subBranch, false))
25
- clauses.push(`(${subClauses.join(` ${key.toUpperCase()} `)})`)
26
- } else {
27
- clauses.push(buildConstraint(key, value, WLModel, meta))
20
+ const bindings = []
21
+
22
+ // Recursively build WHERE clause
23
+ function recurse(branch) {
24
+ const conditions = []
25
+
26
+ // Handle AND conditions
27
+ if (branch.and && Array.isArray(branch.and)) {
28
+ const andConditions = branch.and.map((condition) => recurse(condition))
29
+ const andClauses = andConditions
30
+ .filter((c) => c.clause)
31
+ .map((c) => c.clause)
32
+ if (andClauses.length > 0) {
33
+ conditions.push(`(${andClauses.join(' AND ')})`)
28
34
  }
29
35
  }
30
- return isRoot ? clauses.join(' AND ') : clauses.join(' AND ')
31
- }
32
36
 
33
- return recurse(whereClause)
34
- }
37
+ // Handle OR conditions
38
+ if (branch.or && Array.isArray(branch.or)) {
39
+ const orConditions = branch.or.map((condition) => recurse(condition))
40
+ const orClauses = orConditions
41
+ .filter((c) => c.clause)
42
+ .map((c) => c.clause)
43
+ if (orClauses.length > 0) {
44
+ conditions.push(`(${orClauses.join(' OR ')})`)
45
+ }
46
+ }
35
47
 
36
- function buildConstraint(columnName, constraint, WLModel, meta) {
37
- if (typeof constraint !== 'object' || constraint === null) {
38
- return `${columnName} = ${sqliteEscape(constraint)}`
39
- }
48
+ // Handle field conditions
49
+ Object.keys(branch).forEach((key) => {
50
+ if (key === 'and' || key === 'or') {
51
+ return // Already handled above
52
+ }
40
53
 
41
- const modifierKind = Object.keys(constraint)[0]
42
- const modifier = constraint[modifierKind]
54
+ const value = branch[key]
55
+ let columnName
43
56
 
44
- switch (modifierKind) {
45
- case '<':
46
- return `${columnName} < ${sqliteEscape(modifier)}`
47
- case '<=':
48
- return `${columnName} <= ${sqliteEscape(modifier)}`
49
- case '>':
50
- return `${columnName} > ${sqliteEscape(modifier)}`
51
- case '>=':
52
- return `${columnName} >= ${sqliteEscape(modifier)}`
53
- case '!=':
54
- return `${columnName} != ${sqliteEscape(modifier)}`
55
- case 'nin':
56
- return `${columnName} NOT IN (${modifier.map(sqliteEscape).join(', ')})`
57
- case 'in':
58
- return `${columnName} IN (${modifier.map(sqliteEscape).join(', ')})`
59
- case 'like':
60
- // SQLite uses LIKE with % and _ wildcards (not REGEXP)
61
- let likePattern = modifier
62
- // Convert JavaScript-style wildcards to SQL LIKE wildcards if needed
63
- if (!likePattern.includes('%') && !likePattern.includes('_')) {
64
- // If no wildcards, assume they want contains behavior
65
- likePattern = `%${likePattern}%`
57
+ // Handle table.column format
58
+ if (key.includes('.')) {
59
+ const parts = key.split('.')
60
+ if (parts.length === 2) {
61
+ const [tableName, colName] = parts
62
+ columnName = `\`${tableName}\`.\`${colName}\``
63
+ } else {
64
+ columnName = key
65
+ }
66
+ } else {
67
+ columnName = `\`${key}\``
66
68
  }
67
- let clause = `${columnName} LIKE '${likePattern.replace(/'/g, "''")}'`
68
- if (meta && meta.makeLikeModifierCaseInsensitive === true) {
69
- clause = `LOWER(${columnName}) LIKE LOWER('${likePattern.replace(/'/g, "''")}')`
69
+
70
+ if (typeof value === 'object' && value !== null) {
71
+ // Handle operators
72
+ Object.keys(value).forEach((operator) => {
73
+ const operatorValue = value[operator]
74
+
75
+ switch (operator) {
76
+ case 'in':
77
+ if (Array.isArray(operatorValue) && operatorValue.length > 0) {
78
+ const placeholders = operatorValue.map(() => '?').join(', ')
79
+ conditions.push(`${columnName} IN (${placeholders})`)
80
+ bindings.push(...operatorValue)
81
+ }
82
+ break
83
+ case 'nin':
84
+ if (Array.isArray(operatorValue) && operatorValue.length > 0) {
85
+ const placeholders = operatorValue.map(() => '?').join(', ')
86
+ conditions.push(`${columnName} NOT IN (${placeholders})`)
87
+ bindings.push(...operatorValue)
88
+ }
89
+ break
90
+ case '>':
91
+ conditions.push(`${columnName} > ?`)
92
+ bindings.push(operatorValue)
93
+ break
94
+ case '>=':
95
+ conditions.push(`${columnName} >= ?`)
96
+ bindings.push(operatorValue)
97
+ break
98
+ case '<':
99
+ conditions.push(`${columnName} < ?`)
100
+ bindings.push(operatorValue)
101
+ break
102
+ case '<=':
103
+ conditions.push(`${columnName} <= ?`)
104
+ bindings.push(operatorValue)
105
+ break
106
+ case '!=':
107
+ case 'ne':
108
+ conditions.push(`${columnName} != ?`)
109
+ bindings.push(operatorValue)
110
+ break
111
+ case 'like':
112
+ if (meta && meta.makeLikeModifierCaseInsensitive === true) {
113
+ conditions.push(`LOWER(${columnName}) LIKE LOWER(?)`)
114
+ } else {
115
+ conditions.push(`${columnName} LIKE ?`)
116
+ }
117
+ bindings.push(operatorValue)
118
+ break
119
+ case 'contains':
120
+ conditions.push(`${columnName} LIKE ?`)
121
+ bindings.push(`%${operatorValue}%`)
122
+ break
123
+ case 'startsWith':
124
+ conditions.push(`${columnName} LIKE ?`)
125
+ bindings.push(`${operatorValue}%`)
126
+ break
127
+ case 'endsWith':
128
+ conditions.push(`${columnName} LIKE ?`)
129
+ bindings.push(`%${operatorValue}`)
130
+ break
131
+ default:
132
+ conditions.push(`${columnName} = ?`)
133
+ bindings.push(operatorValue)
134
+ }
135
+ })
136
+ } else {
137
+ // Simple equality
138
+ conditions.push(`${columnName} = ?`)
139
+ bindings.push(value)
70
140
  }
71
- return clause
72
- default:
73
- throw new Error(
74
- `Consistency violation: \`where\` clause modifier \`${modifierKind}\` is not valid! This should never happen-- a stage 3 query should have already been normalized in Waterline core.`
75
- )
76
- }
77
- }
141
+ })
78
142
 
79
- function sqliteEscape(value) {
80
- if (typeof value === 'string') {
81
- return `'${value.replace(/'/g, "''")}'`
82
- }
83
- if (typeof value === 'boolean') {
84
- // Match the decimal format that SQLite stores (1.0, 0.0)
85
- return value ? '1.0' : '0.0'
143
+ return { clause: conditions.join(' AND ') }
86
144
  }
87
- if (value === null) {
88
- return 'NULL'
89
- }
90
- return value
145
+
146
+ const result = recurse(whereClause)
147
+ return { clause: result.clause, bindings }
91
148
  }
@@ -75,18 +75,27 @@ module.exports = function compileStatement(statement) {
75
75
 
76
76
  // Handle regular SELECT statements
77
77
  if (statement.select) {
78
- // Determine parent table for ORDER BY disambiguation
79
- let parentTable = null
80
- if (statement.from) {
81
- if (statement.from.includes(' as ')) {
82
- parentTable = statement.from.split(' as ')[0].trim()
83
- } else {
84
- parentTable = statement.from
85
- }
86
- }
87
78
  const hasJoins =
88
79
  statement.leftOuterJoin && statement.leftOuterJoin.length > 0
89
80
 
81
+ // Build a map of column names to their qualified table.column references
82
+ // This helps resolve unqualified ORDER BY columns
83
+ const columnToTable = {}
84
+ if (Array.isArray(statement.select)) {
85
+ statement.select.forEach((col) => {
86
+ if (col.includes('.')) {
87
+ const colWithoutAlias = col.includes(' as ')
88
+ ? col.split(' as ')[0].trim()
89
+ : col
90
+ const parts = colWithoutAlias.split('.')
91
+ if (parts.length === 2) {
92
+ const [tableName, columnName] = parts
93
+ columnToTable[columnName] = tableName
94
+ }
95
+ }
96
+ })
97
+ }
98
+
90
99
  // SELECT clause
91
100
  if (Array.isArray(statement.select) && statement.select.length > 0) {
92
101
  const selectColumns = statement.select.map((col) => {
@@ -179,10 +188,10 @@ module.exports = function compileStatement(statement) {
179
188
 
180
189
  // WHERE clause
181
190
  if (statement.where) {
182
- const whereResult = buildWhereClause(statement.where)
183
- if (whereResult.clause) {
184
- sql += ` WHERE ${whereResult.clause}`
185
- bindings = bindings.concat(whereResult.bindings || [])
191
+ const whereClause = buildSqliteWhereClause(statement.where)
192
+ if (whereClause.clause) {
193
+ sql += ` WHERE ${whereClause.clause}`
194
+ bindings = bindings.concat(whereClause.bindings || [])
186
195
  }
187
196
  }
188
197
 
@@ -201,9 +210,9 @@ module.exports = function compileStatement(statement) {
201
210
  return `\`${tableName}\`.\`${columnName}\` ASC`
202
211
  }
203
212
  }
204
- // If there are JOINs and column is unqualified, prefix with parent table
205
- if (hasJoins && parentTable) {
206
- return `\`${parentTable}\`.\`${orderItem}\` ASC`
213
+ // If column is unqualified, look up which table it belongs to from SELECT
214
+ if (hasJoins && columnToTable[orderItem]) {
215
+ return `\`${columnToTable[orderItem]}\`.\`${orderItem}\` ASC`
207
216
  }
208
217
  return `\`${orderItem}\` ASC`
209
218
  }
@@ -218,9 +227,9 @@ module.exports = function compileStatement(statement) {
218
227
  return `\`${tableName}\`.\`${columnName}\` ${direction}`
219
228
  }
220
229
  }
221
- // If there are JOINs and column is unqualified, prefix with parent table
222
- if (hasJoins && parentTable) {
223
- return `\`${parentTable}\`.\`${key}\` ${direction}`
230
+ // If column is unqualified, look up which table it belongs to from SELECT
231
+ if (hasJoins && columnToTable[key]) {
232
+ return `\`${columnToTable[key]}\`.\`${key}\` ${direction}`
224
233
  }
225
234
  return `\`${key}\` ${direction}`
226
235
  }
@@ -242,141 +251,3 @@ module.exports = function compileStatement(statement) {
242
251
 
243
252
  return { sql, bindings }
244
253
  }
245
-
246
- /**
247
- * Build WHERE clause from Waterline criteria
248
- */
249
- function buildWhereClause(whereObj) {
250
- if (!whereObj || typeof whereObj !== 'object') {
251
- return { clause: '', bindings: [] }
252
- }
253
-
254
- const conditions = []
255
- const bindings = []
256
-
257
- // Handle AND conditions
258
- if (whereObj.and && Array.isArray(whereObj.and)) {
259
- const andConditions = []
260
- whereObj.and.forEach((condition) => {
261
- const result = buildWhereClause(condition)
262
- if (result.clause) {
263
- andConditions.push(result.clause)
264
- bindings.push(...result.bindings)
265
- }
266
- })
267
- if (andConditions.length > 0) {
268
- conditions.push(`(${andConditions.join(' AND ')})`)
269
- }
270
- }
271
-
272
- // Handle OR conditions
273
- if (whereObj.or && Array.isArray(whereObj.or)) {
274
- const orConditions = []
275
- whereObj.or.forEach((condition) => {
276
- const result = buildWhereClause(condition)
277
- if (result.clause) {
278
- orConditions.push(result.clause)
279
- bindings.push(...result.bindings)
280
- }
281
- })
282
- if (orConditions.length > 0) {
283
- conditions.push(`(${orConditions.join(' OR ')})`)
284
- }
285
- }
286
-
287
- // Handle field conditions
288
- Object.keys(whereObj).forEach((key) => {
289
- if (key === 'and' || key === 'or') {
290
- return // Already handled above
291
- }
292
-
293
- const value = whereObj[key]
294
- let columnName
295
-
296
- // Handle table.column format
297
- if (key.includes('.')) {
298
- const parts = key.split('.')
299
- if (parts.length === 2) {
300
- const [tableName, colName] = parts
301
- columnName = `\`${tableName}\`.\`${colName}\``
302
- } else {
303
- columnName = key // fallback for complex expressions
304
- }
305
- } else {
306
- columnName = `\`${key}\``
307
- }
308
-
309
- if (typeof value === 'object' && value !== null) {
310
- // Handle operators
311
- Object.keys(value).forEach((operator) => {
312
- const operatorValue = value[operator]
313
-
314
- switch (operator) {
315
- case 'in':
316
- if (Array.isArray(operatorValue) && operatorValue.length > 0) {
317
- const placeholders = operatorValue.map(() => '?').join(', ')
318
- conditions.push(`${columnName} IN (${placeholders})`)
319
- bindings.push(...operatorValue)
320
- }
321
- break
322
- case 'nin':
323
- if (Array.isArray(operatorValue) && operatorValue.length > 0) {
324
- const placeholders = operatorValue.map(() => '?').join(', ')
325
- conditions.push(`${columnName} NOT IN (${placeholders})`)
326
- bindings.push(...operatorValue)
327
- }
328
- break
329
- case '>':
330
- conditions.push(`${columnName} > ?`)
331
- bindings.push(operatorValue)
332
- break
333
- case '>=':
334
- conditions.push(`${columnName} >= ?`)
335
- bindings.push(operatorValue)
336
- break
337
- case '<':
338
- conditions.push(`${columnName} < ?`)
339
- bindings.push(operatorValue)
340
- break
341
- case '<=':
342
- conditions.push(`${columnName} <= ?`)
343
- bindings.push(operatorValue)
344
- break
345
- case '!=':
346
- case 'ne':
347
- conditions.push(`${columnName} != ?`)
348
- bindings.push(operatorValue)
349
- break
350
- case 'like':
351
- conditions.push(`${columnName} LIKE ?`)
352
- bindings.push(operatorValue)
353
- break
354
- case 'contains':
355
- conditions.push(`${columnName} LIKE ?`)
356
- bindings.push(`%${operatorValue}%`)
357
- break
358
- case 'startsWith':
359
- conditions.push(`${columnName} LIKE ?`)
360
- bindings.push(`${operatorValue}%`)
361
- break
362
- case 'endsWith':
363
- conditions.push(`${columnName} LIKE ?`)
364
- bindings.push(`%${operatorValue}`)
365
- break
366
- default:
367
- conditions.push(`${columnName} = ?`)
368
- bindings.push(operatorValue)
369
- }
370
- })
371
- } else {
372
- // Simple equality
373
- conditions.push(`${columnName} = ?`)
374
- bindings.push(value)
375
- }
376
- })
377
-
378
- return {
379
- clause: conditions.join(' AND '),
380
- bindings: bindings
381
- }
382
- }
@@ -42,7 +42,8 @@ module.exports = function processEachRecord(options) {
42
42
  // Update the orm object with the keyed collections
43
43
  orm.collections = collections
44
44
 
45
- // Process each record
45
+ // Process each record using waterline-utils' eachRecordDeep
46
+ // which handles recursively processing populated associations
46
47
  eachRecordDeep(
47
48
  records,
48
49
  (record, WLModel) => {
@@ -63,9 +64,17 @@ module.exports = function processEachRecord(options) {
63
64
  if (columnName in record) {
64
65
  switch (attrDef.type) {
65
66
  case 'boolean':
66
- // SQLite stores booleans as integers, so we need to convert them
67
+ // SQLite stores booleans as integers (0 = false, 1 = true)
68
+ // Values may come back as numbers (1, 1.0) or strings ('1', '1.0')
67
69
  if (typeof record[columnName] !== 'boolean') {
68
- record[columnName] = record[columnName] === 1
70
+ const rawValue = record[columnName]
71
+ if (rawValue !== undefined && rawValue !== null) {
72
+ const numericValue =
73
+ typeof rawValue === 'string'
74
+ ? parseFloat(rawValue)
75
+ : rawValue
76
+ record[columnName] = numericValue !== 0
77
+ }
69
78
  }
70
79
  break
71
80
 
@@ -25,21 +25,24 @@ module.exports = function processNativeRecord(nativeRecord, WLModel, meta) {
25
25
  '2nd argument must be a WLModel, and it has to have a `definition` property for this utility to work.'
26
26
  )
27
27
 
28
- // Check out each known attribute...
28
+ // Check out each known attribute and process by column name
29
+ // NOTE: We do NOT rename columnName to attrName here - that's Waterline's job.
30
+ // The adapter must return records with column names, not attribute names.
29
31
  Object.entries(WLModel.attributes).forEach(([attrName, attrDef]) => {
30
- const phRecordKey = attrDef.columnName
32
+ // Use columnName if defined, otherwise fall back to attribute name
33
+ const columnName = attrDef.columnName || attrName
31
34
 
32
35
  // Handle JSON type
33
36
  if (
34
37
  attrDef.type === 'json' &&
35
- typeof nativeRecord[phRecordKey] === 'string'
38
+ typeof nativeRecord[columnName] === 'string'
36
39
  ) {
37
40
  try {
38
- nativeRecord[phRecordKey] = JSON.parse(nativeRecord[phRecordKey])
41
+ nativeRecord[columnName] = JSON.parse(nativeRecord[columnName])
39
42
  } catch (e) {
40
43
  // If parsing fails, leave the value as-is
41
44
  console.warn(
42
- `Failed to parse JSON for attribute ${attrName}: ${e.message}`
45
+ `Failed to parse JSON for column ${columnName}: ${e.message}`
43
46
  )
44
47
  }
45
48
  }
@@ -47,57 +50,45 @@ module.exports = function processNativeRecord(nativeRecord, WLModel, meta) {
47
50
  // Handle Date type
48
51
  if (
49
52
  attrDef.type === 'ref' &&
50
- typeof nativeRecord[phRecordKey] === 'string'
53
+ typeof nativeRecord[columnName] === 'string'
51
54
  ) {
52
- const timestamp = Date.parse(nativeRecord[phRecordKey])
55
+ const timestamp = Date.parse(nativeRecord[columnName])
53
56
  if (!isNaN(timestamp)) {
54
- nativeRecord[phRecordKey] = new Date(timestamp)
57
+ nativeRecord[columnName] = new Date(timestamp)
55
58
  }
56
59
  }
57
60
 
58
61
  // Handle Number type - SQLite returns numbers as strings with decimals
59
- if (
60
- attrDef.type === 'number' &&
61
- typeof nativeRecord[phRecordKey] === 'string'
62
- ) {
63
- const numericValue = parseFloat(nativeRecord[phRecordKey])
64
- if (!isNaN(numericValue)) {
65
- nativeRecord[phRecordKey] = numericValue
62
+ // Also ensure auto timestamps are numbers
63
+ if (attrDef.type === 'number') {
64
+ if (typeof nativeRecord[columnName] === 'string') {
65
+ const numericValue = parseFloat(nativeRecord[columnName])
66
+ if (!isNaN(numericValue)) {
67
+ nativeRecord[columnName] = numericValue
68
+ }
69
+ }
70
+ // Ensure auto timestamps are numbers (like sails-postgresql does)
71
+ if (
72
+ (attrDef.autoUpdatedAt || attrDef.autoCreatedAt) &&
73
+ nativeRecord[columnName] !== undefined
74
+ ) {
75
+ nativeRecord[columnName] = Number(nativeRecord[columnName])
66
76
  }
67
77
  }
68
78
 
69
79
  // Handle Boolean type
70
80
  if (attrDef.type === 'boolean') {
71
81
  // SQLite stores booleans as integers (0 = false, 1 = true)
72
- // But they come back as strings, so we need to parse them first
73
- const rawValue = nativeRecord[phRecordKey]
82
+ // Values may come back as numbers (1, 1.0) or strings ('1', '1.0')
83
+ const rawValue = nativeRecord[columnName]
74
84
  if (rawValue !== undefined && rawValue !== null) {
75
- const numericValue =
76
- typeof rawValue === 'string' ? parseFloat(rawValue) : rawValue
77
- nativeRecord[phRecordKey] = numericValue === 0 ? false : true
85
+ if (typeof rawValue !== 'boolean') {
86
+ const numericValue =
87
+ typeof rawValue === 'string' ? parseFloat(rawValue) : rawValue
88
+ nativeRecord[columnName] = numericValue !== 0
89
+ }
78
90
  }
79
91
  }
80
-
81
- const isForeignKey = !!attrDef.model
82
- // Sanity checks:
83
- if (isForeignKey) {
84
- assert(
85
- attrDef.foreignKey,
86
- 'attribute has a `model` property, but wl-schema did not give it `foreignKey: true`!'
87
- )
88
- } else {
89
- assert(
90
- !attrDef.foreignKey,
91
- 'wl-schema gave this attribute `foreignKey: true`, but it has no `model` property!'
92
- )
93
- }
94
-
95
- if (!isForeignKey) return
96
- if (nativeRecord[phRecordKey] === undefined) return // This is weird, but WL core deals with warning about it.
97
- if (nativeRecord[phRecordKey] === null) return
98
-
99
- // For SQLite, we don't need to do any special processing for foreign keys
100
- // as they are typically just stored as integers or strings.
101
92
  })
102
93
 
103
94
  return nativeRecord
@@ -22,53 +22,66 @@ module.exports = {
22
22
  },
23
23
 
24
24
  fn: function (inputs, exits) {
25
- const db = inputs.connection
26
- const sequenceName = inputs.sequenceName
27
- const newSequenceValue = inputs.sequenceValue
25
+ const { connection: db, sequenceName, sequenceValue } = inputs
28
26
 
29
- // Parse the sequence name to get the actual table name
30
- // PostgreSQL-style sequences are often named like 'user_id_seq', 'users_id_seq', etc.
31
- // The table name should be the first string before the first underscore
32
- let tableName = sequenceName
33
-
34
- // Handle PostgreSQL-style sequence names
35
- if (sequenceName.includes('_')) {
36
- // Extract the table name as the first part before the first underscore
37
- tableName = sequenceName.split('_')[0]
38
- }
39
-
40
- try {
41
- // First, check if the table exists
42
- const tableExists = db
43
- .prepare(
44
- "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
45
- )
46
- .get(tableName)
47
-
48
- if (!tableExists) {
49
- return exits.notFound(new Error(`Table '${tableName}' not found.`))
27
+ // Parse table name from PostgreSQL-style sequence name
28
+ // Format is: tablename_columnname_seq
29
+ // e.g., 'invoice_items_id_seq' -> table 'invoice_items', column 'id'
30
+ // e.g., 'alter__id_seq' -> table 'alter', column '_id'
31
+ // e.g., 'users_user_id_seq' -> table 'users', column 'user_id'
32
+ //
33
+ // Since we can't reliably split table/column when both contain underscores,
34
+ // we try progressively shorter prefixes until we find an existing table.
35
+ let tableName = null
36
+ if (sequenceName.endsWith('_seq')) {
37
+ const withoutSeq = sequenceName.slice(0, -4) // Remove '_seq'
38
+ // Try progressively shorter prefixes to find the table name
39
+ // Start from the longest possible table name and work backwards
40
+ let candidate = withoutSeq
41
+ while (candidate.includes('_')) {
42
+ const lastUnderscoreIdx = candidate.lastIndexOf('_')
43
+ candidate = candidate.slice(0, lastUnderscoreIdx)
44
+ // Check if this table exists
45
+ const exists = db
46
+ .prepare(
47
+ "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
48
+ )
49
+ .get(candidate)
50
+ if (exists) {
51
+ tableName = candidate
52
+ break
53
+ }
50
54
  }
55
+ // If no table found with underscores, try the full name without _seq
56
+ if (!tableName) {
57
+ const exists = db
58
+ .prepare(
59
+ "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
60
+ )
61
+ .get(withoutSeq)
62
+ if (exists) {
63
+ tableName = withoutSeq
64
+ }
65
+ }
66
+ }
51
67
 
52
- // If the table exists, update the sequence
53
- const updateStmt = db.prepare(
54
- 'UPDATE sqlite_sequence SET seq = ? WHERE name = ?'
68
+ // If still no table found, exit with notFound
69
+ if (!tableName) {
70
+ return exits.notFound(
71
+ new Error(`Could not find table for sequence '${sequenceName}'.`)
55
72
  )
56
- const updateResult = updateStmt.run(newSequenceValue - 1, tableName)
73
+ }
57
74
 
58
- if (updateResult.changes === 0) {
59
- // If no rows were updated, it means the table doesn't have an autoincrement column
60
- // We'll insert a new row in this case
61
- const insertStmt = db.prepare(
62
- 'INSERT INTO sqlite_sequence (name, seq) VALUES (?, ?)'
63
- )
64
- insertStmt.run(tableName, newSequenceValue - 1)
65
- }
75
+ try {
76
+ // Use INSERT OR REPLACE to handle both insert and update in one statement
77
+ db.prepare(
78
+ 'INSERT OR REPLACE INTO sqlite_sequence (name, seq) VALUES (?, ?)'
79
+ ).run(tableName, sequenceValue - 1)
66
80
 
67
81
  return exits.success()
68
82
  } catch (error) {
69
- // Handle the case where sqlite_sequence doesn't exist
83
+ // sqlite_sequence doesn't exist = no AUTOINCREMENT tables yet, which is fine
70
84
  if (error.message.includes('no such table: sqlite_sequence')) {
71
- // This is not an error condition - it just means no tables with AUTOINCREMENT have been created yet
72
85
  return exits.success()
73
86
  }
74
87
  return exits.error(error)
@@ -60,12 +60,12 @@ module.exports = {
60
60
 
61
61
  try {
62
62
  let sumQuery = `SELECT COALESCE(SUM(\`${numericFieldName}\`), 0) as total FROM \`${tableName}\``
63
- if (whereClause) {
64
- sumQuery += ` WHERE ${whereClause}`
63
+ if (whereClause.clause) {
64
+ sumQuery += ` WHERE ${whereClause.clause}`
65
65
  }
66
66
 
67
67
  const stmt = db.prepare(sumQuery)
68
- const result = stmt.get()
68
+ const result = stmt.get(...whereClause.bindings)
69
69
 
70
70
  return exits.success(result.total)
71
71
  } catch (err) {
@@ -64,7 +64,7 @@ module.exports = {
64
64
  return exits.error(e)
65
65
  }
66
66
 
67
- const sqliteWhere = buildSqliteWhereClause(
67
+ const whereClause = buildSqliteWhereClause(
68
68
  s3q.criteria.where,
69
69
  WLModel,
70
70
  s3q.meta
@@ -78,31 +78,37 @@ module.exports = {
78
78
  try {
79
79
  // Start a transaction only if we're not already in one
80
80
  if (!wasInTransaction) {
81
- db.exec('BEGIN TRANSACTION')
81
+ db.prepare('BEGIN TRANSACTION').run()
82
82
  }
83
83
 
84
84
  let affectedIds = []
85
85
 
86
86
  if (isFetchEnabled) {
87
87
  // Get the IDs of records which match this criteria
88
- const selectSql = sqliteWhere
89
- ? `SELECT \`${pkColumnName}\` FROM \`${tableName}\` WHERE ${sqliteWhere}`
88
+ const selectSql = whereClause.clause
89
+ ? `SELECT \`${pkColumnName}\` FROM \`${tableName}\` WHERE ${whereClause.clause}`
90
90
  : `SELECT \`${pkColumnName}\` FROM \`${tableName}\``
91
91
  const selectStmt = db.prepare(selectSql)
92
- affectedIds = selectStmt.all().map((row) => row[pkColumnName])
92
+ affectedIds = selectStmt
93
+ .all(...whereClause.bindings)
94
+ .map((row) => row[pkColumnName])
93
95
  }
94
96
 
95
97
  // Prepare the UPDATE statement
96
98
  const setClauses = Object.entries(s3q.valuesToSet)
97
99
  .map(([column, value]) => `\`${column}\` = ?`)
98
100
  .join(', ')
99
- const updateSql = sqliteWhere
100
- ? `UPDATE \`${tableName}\` SET ${setClauses} WHERE ${sqliteWhere}`
101
+ const updateSql = whereClause.clause
102
+ ? `UPDATE \`${tableName}\` SET ${setClauses} WHERE ${whereClause.clause}`
101
103
  : `UPDATE \`${tableName}\` SET ${setClauses}`
102
104
  const updateStmt = db.prepare(updateSql)
103
105
 
104
- // Execute the UPDATE
105
- const updateInfo = updateStmt.run(...Object.values(s3q.valuesToSet))
106
+ // Execute the UPDATE with values and where bindings
107
+ const updateBindings = [
108
+ ...Object.values(s3q.valuesToSet),
109
+ ...whereClause.bindings
110
+ ]
111
+ const updateInfo = updateStmt.run(...updateBindings)
106
112
 
107
113
  // Handle case where pk value was changed
108
114
  if (
@@ -117,7 +123,7 @@ module.exports = {
117
123
  affectedIds.length > 1
118
124
  ) {
119
125
  if (!wasInTransaction) {
120
- db.exec('ROLLBACK')
126
+ db.prepare('ROLLBACK').run()
121
127
  }
122
128
  return exits.error(
123
129
  new Error(
@@ -129,7 +135,7 @@ module.exports = {
129
135
  // If fetch is not enabled, we're done
130
136
  if (!isFetchEnabled) {
131
137
  if (!wasInTransaction) {
132
- db.exec('COMMIT')
138
+ db.prepare('COMMIT').run()
133
139
  }
134
140
  return exits.success()
135
141
  }
@@ -145,12 +151,12 @@ module.exports = {
145
151
  })
146
152
 
147
153
  if (!wasInTransaction) {
148
- db.exec('COMMIT')
154
+ db.prepare('COMMIT').run()
149
155
  }
150
156
  return exits.success(phRecords)
151
157
  } catch (err) {
152
158
  if (!wasInTransaction) {
153
- db.exec('ROLLBACK')
159
+ db.prepare('ROLLBACK').run()
154
160
  }
155
161
  err = processNativeError(err)
156
162
  if (err.footprint && err.footprint.identity === 'notUnique') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sails-sqlite",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "SQLite adapter for Sails/Waterline",
5
5
  "main": "lib",
6
6
  "directories": {