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
|
@@ -1,30 +1,11 @@
|
|
|
1
|
-
const processEachRecord = require('./private/process-each-record')
|
|
2
|
-
const generateJoinSqlQuery = require('./private/generate-join-sql-query')
|
|
3
|
-
|
|
4
1
|
module.exports = {
|
|
5
2
|
friendlyName: 'Join',
|
|
6
|
-
|
|
7
3
|
description: 'Perform a join operation in SQLite using better-sqlite3.',
|
|
8
|
-
|
|
9
4
|
inputs: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
example: '==='
|
|
14
|
-
},
|
|
15
|
-
models: {
|
|
16
|
-
description:
|
|
17
|
-
'An object containing all of the model definitions that have been registered.',
|
|
18
|
-
required: true,
|
|
19
|
-
example: '==='
|
|
20
|
-
},
|
|
21
|
-
query: {
|
|
22
|
-
description: 'A normalized Waterline Stage Three Query.',
|
|
23
|
-
required: true,
|
|
24
|
-
example: '==='
|
|
25
|
-
}
|
|
5
|
+
query: require('../constants/query.input'),
|
|
6
|
+
connection: require('../constants/connection.input'),
|
|
7
|
+
dryOrm: require('../constants/dry-orm.input')
|
|
26
8
|
},
|
|
27
|
-
|
|
28
9
|
exits: {
|
|
29
10
|
success: {
|
|
30
11
|
description: 'The query was run successfully.',
|
|
@@ -34,60 +15,245 @@ module.exports = {
|
|
|
34
15
|
description: 'An error occurred while performing the query.'
|
|
35
16
|
}
|
|
36
17
|
},
|
|
18
|
+
fn: function (inputs, exits) {
|
|
19
|
+
const _ = require('@sailshq/lodash')
|
|
20
|
+
const async = require('async')
|
|
21
|
+
const WLUtils = require('waterline-utils')
|
|
22
|
+
const processEachRecord = require('./private/process-each-record')
|
|
23
|
+
const compileStatement = require('./private/compile-statement')
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
25
|
+
const { query, connection, dryOrm } = inputs
|
|
26
|
+
const models = dryOrm.models
|
|
27
|
+
|
|
28
|
+
let hasReturned = false
|
|
29
|
+
|
|
30
|
+
// Find the model definition
|
|
31
|
+
const model = models[query.using]
|
|
32
|
+
if (!model) {
|
|
33
|
+
if (hasReturned) return
|
|
34
|
+
hasReturned = true
|
|
35
|
+
return exits.error(new Error(`No model found for table: ${query.using}`))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get primary key info
|
|
39
|
+
const primaryKeyAttr = model.primaryKey
|
|
40
|
+
const primaryKeyColumnName =
|
|
41
|
+
model.definition[primaryKeyAttr].columnName || primaryKeyAttr
|
|
42
|
+
|
|
43
|
+
// Build statements
|
|
44
|
+
const statements = WLUtils.joins.convertJoinCriteria({
|
|
45
|
+
query,
|
|
46
|
+
getPk: function getPk(tableName) {
|
|
47
|
+
let targetModel = null
|
|
48
|
+
for (const modelIdentity in models) {
|
|
49
|
+
if (models[modelIdentity].tableName === tableName) {
|
|
50
|
+
targetModel = models[modelIdentity]
|
|
51
|
+
break
|
|
55
52
|
}
|
|
53
|
+
}
|
|
54
|
+
if (!targetModel) {
|
|
55
|
+
throw new Error('Invalid parent table name')
|
|
56
|
+
}
|
|
57
|
+
const pkAttrName = targetModel.primaryKey
|
|
58
|
+
const pkColumnName =
|
|
59
|
+
targetModel.definition[pkAttrName].columnName || pkAttrName
|
|
60
|
+
return pkColumnName
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Run parent query
|
|
65
|
+
const compiledQuery = compileStatement(statements.parentStatement)
|
|
66
|
+
const db = connection
|
|
67
|
+
const stmt = db.prepare(compiledQuery.sql)
|
|
68
|
+
const parentResults = stmt.all(...(compiledQuery.bindings || []))
|
|
69
|
+
|
|
70
|
+
// Early exit if no joins or no results
|
|
71
|
+
if (!_.has(query, 'joins') || !parentResults.length) {
|
|
72
|
+
if (hasReturned) return
|
|
73
|
+
hasReturned = true
|
|
74
|
+
return exits.success(parentResults)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Detect child records
|
|
78
|
+
const sortedResults = WLUtils.joins.detectChildrenRecords(
|
|
79
|
+
primaryKeyColumnName,
|
|
80
|
+
parentResults
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Initialize query cache
|
|
84
|
+
const queryCache = WLUtils.joins.queryCache()
|
|
85
|
+
|
|
86
|
+
// Process instructions
|
|
87
|
+
_.each(statements.instructions, function (val, key) {
|
|
88
|
+
const popInstructions = val.instructions
|
|
89
|
+
const strategy = val.strategy.strategy
|
|
90
|
+
const parentModel = models[_.first(popInstructions).parent]
|
|
91
|
+
|
|
92
|
+
if (!parentModel) {
|
|
93
|
+
throw new Error('Invalid parent model in instructions')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const pkAttr = parentModel.primaryKey
|
|
97
|
+
const pkColumnName = parentModel.definition[pkAttr].columnName || pkAttr
|
|
98
|
+
|
|
99
|
+
let alias, keyName
|
|
100
|
+
if (val.strategy && val.strategy.strategy === 1) {
|
|
101
|
+
alias = _.first(popInstructions).alias
|
|
102
|
+
keyName = _.first(popInstructions).parentKey
|
|
103
|
+
} else {
|
|
104
|
+
alias = _.first(popInstructions).alias
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_.each(sortedResults.parents, function (parentRecord) {
|
|
108
|
+
const cache = {
|
|
109
|
+
attrName: key,
|
|
110
|
+
parentPkAttr: pkColumnName,
|
|
111
|
+
belongsToPkValue: parentRecord[pkColumnName],
|
|
112
|
+
keyName: keyName || alias,
|
|
113
|
+
type: strategy
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const childKey = _.first(popInstructions).childKey
|
|
117
|
+
const parentKey = _.first(popInstructions).parentKey
|
|
56
118
|
|
|
57
|
-
|
|
58
|
-
|
|
119
|
+
const records = _.filter(
|
|
120
|
+
sortedResults.children[alias],
|
|
121
|
+
function (child) {
|
|
122
|
+
if (strategy === 3) {
|
|
123
|
+
return child._parent_fk === parentRecord[parentKey]
|
|
124
|
+
}
|
|
125
|
+
return child[childKey] === parentRecord[parentKey]
|
|
59
126
|
}
|
|
127
|
+
)
|
|
60
128
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
129
|
+
if (strategy === 3) {
|
|
130
|
+
_.each(records, function (record) {
|
|
131
|
+
delete record._parent_fk
|
|
132
|
+
})
|
|
64
133
|
}
|
|
65
|
-
})
|
|
66
134
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
joinCriteria,
|
|
70
|
-
models,
|
|
71
|
-
query
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
// Execute the query using the database connection from the datastore
|
|
75
|
-
const db = datastore.manager
|
|
76
|
-
const stmt = db.prepare(sql)
|
|
77
|
-
const results = stmt.all(...bindings)
|
|
78
|
-
|
|
79
|
-
// Process results through the join utility
|
|
80
|
-
const processedResults = joins.processJoinResults({
|
|
81
|
-
query,
|
|
82
|
-
records: results,
|
|
83
|
-
orm: {
|
|
84
|
-
collections: models
|
|
135
|
+
if (records.length) {
|
|
136
|
+
cache.records = records
|
|
85
137
|
}
|
|
138
|
+
|
|
139
|
+
queryCache.set(cache)
|
|
86
140
|
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Set parents
|
|
144
|
+
queryCache.setParents(sortedResults.parents)
|
|
87
145
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
146
|
+
// Single-query path (no child statements)
|
|
147
|
+
if (!statements.childStatements || !statements.childStatements.length) {
|
|
148
|
+
const combinedResults = queryCache.combineRecords() || []
|
|
149
|
+
const orm = { collections: models }
|
|
150
|
+
processEachRecord({
|
|
151
|
+
records: combinedResults,
|
|
152
|
+
identity: model.identity,
|
|
153
|
+
orm: orm
|
|
154
|
+
})
|
|
155
|
+
if (hasReturned) return
|
|
156
|
+
hasReturned = true
|
|
157
|
+
return exits.success(combinedResults)
|
|
91
158
|
}
|
|
159
|
+
|
|
160
|
+
// Multi-query path (process child statements)
|
|
161
|
+
const parentKeys = _.map(queryCache.getParents(), function (record) {
|
|
162
|
+
return record[primaryKeyColumnName]
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
async.each(
|
|
166
|
+
statements.childStatements,
|
|
167
|
+
function (template, next) {
|
|
168
|
+
// Handle IN queries
|
|
169
|
+
if (template.queryType === 'in') {
|
|
170
|
+
const inClause = _.pullAt(
|
|
171
|
+
template.statement.where.and,
|
|
172
|
+
template.statement.where.and.length - 1
|
|
173
|
+
)
|
|
174
|
+
const clause = _.first(inClause)
|
|
175
|
+
_.each(clause, function (val) {
|
|
176
|
+
val.in = parentKeys
|
|
177
|
+
})
|
|
178
|
+
template.statement.where.and.push(clause)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Handle UNION queries with special case for per-entity pagination
|
|
182
|
+
if (template.queryType === 'union') {
|
|
183
|
+
const unionStatements = []
|
|
184
|
+
_.each(parentKeys, function (parentPk) {
|
|
185
|
+
const unionStatement = _.merge({}, template.statement)
|
|
186
|
+
const andClause = _.pullAt(
|
|
187
|
+
unionStatement.where.and,
|
|
188
|
+
unionStatement.where.and.length - 1
|
|
189
|
+
)
|
|
190
|
+
_.each(_.first(andClause), function (val, key) {
|
|
191
|
+
_.first(andClause)[key] = parentPk
|
|
192
|
+
})
|
|
193
|
+
unionStatement.where.and.push(_.first(andClause))
|
|
194
|
+
unionStatements.push(unionStatement)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
if (unionStatements.length) {
|
|
198
|
+
// Check if this is per-entity pagination (has LIMIT/OFFSET)
|
|
199
|
+
const hasPerEntityPagination =
|
|
200
|
+
unionStatements[0].limit || unionStatements[0].skip
|
|
201
|
+
|
|
202
|
+
if (hasPerEntityPagination && unionStatements.length > 1) {
|
|
203
|
+
// SQLite-savvy approach: Execute separate queries for per-entity pagination
|
|
204
|
+
const allChildResults = []
|
|
205
|
+
|
|
206
|
+
_.each(unionStatements, function (singleStatement) {
|
|
207
|
+
const compiledQuery = compileStatement(singleStatement)
|
|
208
|
+
const stmt = db.prepare(compiledQuery.sql)
|
|
209
|
+
const results = stmt.all(...(compiledQuery.bindings || []))
|
|
210
|
+
allChildResults.push(...results)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Extend cache with combined results
|
|
214
|
+
queryCache.extend(allChildResults, template.instructions)
|
|
215
|
+
return next()
|
|
216
|
+
} else {
|
|
217
|
+
// Standard UNION approach for non-pagination cases
|
|
218
|
+
template.statement = { unionAll: unionStatements }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!template.statement) {
|
|
224
|
+
return next()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Run child query
|
|
228
|
+
const childCompiledQuery = compileStatement(template.statement)
|
|
229
|
+
const childStmt = db.prepare(childCompiledQuery.sql)
|
|
230
|
+
const childResults = childStmt.all(
|
|
231
|
+
...(childCompiledQuery.bindings || [])
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// Extend cache
|
|
235
|
+
queryCache.extend(childResults, template.instructions)
|
|
236
|
+
next()
|
|
237
|
+
},
|
|
238
|
+
function (err) {
|
|
239
|
+
if (hasReturned) return
|
|
240
|
+
|
|
241
|
+
if (err) {
|
|
242
|
+
hasReturned = true
|
|
243
|
+
return exits.error(err)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Final combine and return
|
|
247
|
+
const combinedResults = queryCache.combineRecords() || []
|
|
248
|
+
const orm = { collections: models }
|
|
249
|
+
processEachRecord({
|
|
250
|
+
records: combinedResults,
|
|
251
|
+
identity: model.identity,
|
|
252
|
+
orm: orm
|
|
253
|
+
})
|
|
254
|
+
hasReturned = true
|
|
255
|
+
return exits.success(combinedResults)
|
|
256
|
+
}
|
|
257
|
+
)
|
|
92
258
|
}
|
|
93
259
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module dependencies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lease Connection
|
|
7
|
+
*
|
|
8
|
+
* Get a dedicated connection from the datastore for use in transactions.
|
|
9
|
+
* For SQLite, this returns the same connection manager since SQLite is single-threaded.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
friendlyName: 'Lease connection',
|
|
14
|
+
|
|
15
|
+
description:
|
|
16
|
+
'Get a dedicated connection from the datastore for use in transactions.',
|
|
17
|
+
|
|
18
|
+
moreInfoUrl:
|
|
19
|
+
'https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md',
|
|
20
|
+
|
|
21
|
+
inputs: {
|
|
22
|
+
manager: {
|
|
23
|
+
description: 'The connection manager instance to get a connection from.',
|
|
24
|
+
example: '===',
|
|
25
|
+
required: true
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
meta: {
|
|
29
|
+
description: 'Additional options for this query.',
|
|
30
|
+
example: '==='
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
exits: {
|
|
35
|
+
failed: {
|
|
36
|
+
description: 'Could not get a connection to the database.'
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
success: {
|
|
40
|
+
description: 'A connection was successfully leased.',
|
|
41
|
+
outputExample: '==='
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
fn: function leaseConnection(inputs, exits) {
|
|
46
|
+
const manager = inputs.manager
|
|
47
|
+
const meta = inputs.meta || {}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// For SQLite, the manager IS the database connection
|
|
51
|
+
// SQLite is single-threaded and doesn't support concurrent transactions
|
|
52
|
+
// The "connection" is actually the database instance
|
|
53
|
+
return exits.success(manager)
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return exits.failed(err)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* @returns {String} [SQLite WHERE clause]
|
|
12
12
|
*/
|
|
13
13
|
module.exports = function buildSqliteWhereClause(whereClause, WLModel, meta) {
|
|
14
|
-
// Handle empty `where` clause.
|
|
15
|
-
if (Object.keys(whereClause).length === 0) {
|
|
14
|
+
// Handle null, undefined, or empty `where` clause.
|
|
15
|
+
if (!whereClause || Object.keys(whereClause).length === 0) {
|
|
16
16
|
return ''
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -57,14 +57,16 @@ function buildConstraint(columnName, constraint, WLModel, meta) {
|
|
|
57
57
|
case 'in':
|
|
58
58
|
return `${columnName} IN (${modifier.map(sqliteEscape).join(', ')})`
|
|
59
59
|
case 'like':
|
|
60
|
+
// SQLite uses LIKE with % and _ wildcards (not REGEXP)
|
|
60
61
|
let likePattern = modifier
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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}%`
|
|
66
|
+
}
|
|
67
|
+
let clause = `${columnName} LIKE '${likePattern.replace(/'/g, "''")}'`
|
|
66
68
|
if (meta && meta.makeLikeModifierCaseInsensitive === true) {
|
|
67
|
-
clause = `LOWER(${columnName})
|
|
69
|
+
clause = `LOWER(${columnName}) LIKE LOWER('${likePattern.replace(/'/g, "''")}')`
|
|
68
70
|
}
|
|
69
71
|
return clause
|
|
70
72
|
default:
|