sails-sqlite 0.2.3 → 0.2.5
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/private/machines/avg-records.js +3 -3
- package/lib/private/machines/count-records.js +3 -3
- package/lib/private/machines/create-manager.js +12 -5
- package/lib/private/machines/destroy-records.js +10 -10
- package/lib/private/machines/find-records.js +5 -8
- package/lib/private/machines/private/build-sqlite-where-clause.js +121 -64
- package/lib/private/machines/private/compile-statement.js +28 -157
- package/lib/private/machines/private/process-each-record.js +12 -3
- package/lib/private/machines/private/process-native-record.js +32 -41
- package/lib/private/machines/set-physical-sequence.js +51 -38
- package/lib/private/machines/sum-records.js +3 -3
- package/lib/private/machines/update-records.js +19 -13
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -41,12 +41,19 @@ module.exports = {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Create database connection with optimized options
|
|
44
|
+
// Determine verbose logging function:
|
|
45
|
+
// - verbose: true -> use console.log
|
|
46
|
+
// - verbose: function -> use custom function
|
|
47
|
+
// - verbose: false/undefined -> disabled
|
|
48
|
+
let verboseFn = null
|
|
49
|
+
if (meta?.verbose === true) {
|
|
50
|
+
verboseFn = console.log
|
|
51
|
+
} else if (typeof meta?.verbose === 'function') {
|
|
52
|
+
verboseFn = meta.verbose
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
const dbOptions = {
|
|
45
|
-
|
|
46
|
-
verbose:
|
|
47
|
-
meta?.verbose || process.env.NODE_ENV === 'development'
|
|
48
|
-
? console.log
|
|
49
|
-
: null,
|
|
56
|
+
verbose: verboseFn,
|
|
50
57
|
// Set timeout for database operations
|
|
51
58
|
timeout: meta?.timeout || 5000,
|
|
52
59
|
// Enable read-only mode if specified
|
|
@@ -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
|
|
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.
|
|
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 =
|
|
78
|
-
? `SELECT * FROM \`${tableName}\` WHERE ${
|
|
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 =
|
|
86
|
-
? `DELETE FROM \`${tableName}\` WHERE ${
|
|
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.
|
|
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.
|
|
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
|
|
103
|
-
const stmt = db.
|
|
104
|
-
|
|
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 {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
54
|
+
const value = branch[key]
|
|
55
|
+
let columnName
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
183
|
-
if (
|
|
184
|
-
sql += ` WHERE ${
|
|
185
|
-
bindings = bindings.concat(
|
|
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
|
|
205
|
-
if (hasJoins &&
|
|
206
|
-
return `\`${
|
|
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
|
|
222
|
-
if (hasJoins &&
|
|
223
|
-
return `\`${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
38
|
+
typeof nativeRecord[columnName] === 'string'
|
|
36
39
|
) {
|
|
37
40
|
try {
|
|
38
|
-
nativeRecord[
|
|
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
|
|
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[
|
|
53
|
+
typeof nativeRecord[columnName] === 'string'
|
|
51
54
|
) {
|
|
52
|
-
const timestamp = Date.parse(nativeRecord[
|
|
55
|
+
const timestamp = Date.parse(nativeRecord[columnName])
|
|
53
56
|
if (!isNaN(timestamp)) {
|
|
54
|
-
nativeRecord[
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
typeof nativeRecord[
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
//
|
|
73
|
-
const rawValue = nativeRecord[
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
26
|
-
const sequenceName = inputs.sequenceName
|
|
27
|
-
const newSequenceValue = inputs.sequenceValue
|
|
25
|
+
const { connection: db, sequenceName, sequenceValue } = inputs
|
|
28
26
|
|
|
29
|
-
// Parse
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
73
|
+
}
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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 =
|
|
89
|
-
? `SELECT \`${pkColumnName}\` FROM \`${tableName}\` WHERE ${
|
|
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
|
|
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 =
|
|
100
|
-
? `UPDATE \`${tableName}\` SET ${setClauses} WHERE ${
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
154
|
+
db.prepare('COMMIT').run()
|
|
149
155
|
}
|
|
150
156
|
return exits.success(phRecords)
|
|
151
157
|
} catch (err) {
|
|
152
158
|
if (!wasInTransaction) {
|
|
153
|
-
db.
|
|
159
|
+
db.prepare('ROLLBACK').run()
|
|
154
160
|
}
|
|
155
161
|
err = processNativeError(err)
|
|
156
162
|
if (err.footprint && err.footprint.identity === 'notUnique') {
|