sails-sqlite 0.0.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/.github/FUNDING.yml +1 -0
- package/.github/workflows/prettier.yml +16 -0
- package/.github/workflows/test.yml +16 -0
- package/.husky/pre-commit +1 -0
- package/.prettierrc.js +5 -0
- package/CHANGELOG.md +161 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/lib/index.js +1104 -0
- package/lib/private/build-std-adapter-method.js +69 -0
- package/lib/private/constants/connection.input.js +15 -0
- package/lib/private/constants/dry-orm.input.js +23 -0
- package/lib/private/constants/meta.input.js +14 -0
- package/lib/private/constants/not-unique.exit.js +16 -0
- package/lib/private/constants/query.input.js +15 -0
- package/lib/private/constants/table-name.input.js +12 -0
- package/lib/private/machines/avg-records.js +74 -0
- 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 +78 -0
- package/lib/private/machines/create-each-record.js +163 -0
- package/lib/private/machines/create-manager.js +174 -0
- package/lib/private/machines/create-record.js +126 -0
- package/lib/private/machines/define-physical-model.js +111 -0
- package/lib/private/machines/destroy-manager.js +87 -0
- package/lib/private/machines/destroy-records.js +114 -0
- package/lib/private/machines/drop-physical-model.js +51 -0
- package/lib/private/machines/find-records.js +120 -0
- package/lib/private/machines/get-connection.js +54 -0
- package/lib/private/machines/join.js +259 -0
- package/lib/private/machines/lease-connection.js +58 -0
- package/lib/private/machines/private/build-sqlite-where-clause.js +91 -0
- package/lib/private/machines/private/compile-statement.js +334 -0
- package/lib/private/machines/private/generate-join-sql-query.js +385 -0
- package/lib/private/machines/private/process-each-record.js +106 -0
- package/lib/private/machines/private/process-native-error.js +104 -0
- package/lib/private/machines/private/process-native-record.js +104 -0
- package/lib/private/machines/private/reify-values-to-set.js +83 -0
- package/lib/private/machines/release-connection.js +70 -0
- package/lib/private/machines/rollback-transaction.js +50 -0
- package/lib/private/machines/set-physical-sequence.js +77 -0
- package/lib/private/machines/sum-records.js +75 -0
- package/lib/private/machines/update-records.js +162 -0
- package/lib/private/machines/verify-model-def.js +38 -0
- package/package.json +58 -5
- package/tests/index.js +88 -0
- package/tests/runner.js +99 -0
- package/tests/transaction.test.js +562 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
const util = require('util')
|
|
3
|
+
const processNativeRecord = require('./private/process-native-record')
|
|
4
|
+
const buildSqliteWhereClause = require('./private/build-sqlite-where-clause')
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
friendlyName: 'Find (records)',
|
|
8
|
+
|
|
9
|
+
description: 'Find record(s) in the SQLite database.',
|
|
10
|
+
|
|
11
|
+
inputs: {
|
|
12
|
+
query: require('../constants/query.input'),
|
|
13
|
+
connection: require('../constants/connection.input'),
|
|
14
|
+
dryOrm: require('../constants/dry-orm.input')
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
exits: {
|
|
18
|
+
success: {
|
|
19
|
+
outputFriendlyName: 'Records',
|
|
20
|
+
outputDescription: 'An array of physical records.',
|
|
21
|
+
outputExample: '===' //[ {===} ]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
fn: async function (inputs, exits) {
|
|
26
|
+
const s3q = inputs.query
|
|
27
|
+
if (s3q.meta && s3q.meta.logSqliteS3Qs) {
|
|
28
|
+
console.log(
|
|
29
|
+
'* * * * * *\nADAPTER (FIND RECORDS):',
|
|
30
|
+
util.inspect(s3q, { depth: 10 }),
|
|
31
|
+
'\n'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tableName = s3q.using
|
|
36
|
+
// Find model by tableName since models is an object, not an array
|
|
37
|
+
let WLModel = null
|
|
38
|
+
for (const modelIdentity in inputs.dryOrm.models) {
|
|
39
|
+
if (inputs.dryOrm.models[modelIdentity].tableName === tableName) {
|
|
40
|
+
WLModel = inputs.dryOrm.models[modelIdentity]
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!WLModel) {
|
|
46
|
+
return exits.error(
|
|
47
|
+
new Error(
|
|
48
|
+
`No model with that tableName (\`${tableName}\`) has been registered with this adapter. Were any unexpected modifications made to the stage 3 query? Could the adapter's internal state have been corrupted? (This error is usually due to a bug in this adapter's implementation.)`
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const db = inputs.connection
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
let sqlQuery = `SELECT `
|
|
57
|
+
|
|
58
|
+
// Handle SELECT clause
|
|
59
|
+
if (s3q.criteria.select) {
|
|
60
|
+
sqlQuery += s3q.criteria.select.map((col) => `\`${col}\``).join(', ')
|
|
61
|
+
} else {
|
|
62
|
+
sqlQuery += '*'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sqlQuery += ` FROM \`${tableName}\``
|
|
66
|
+
|
|
67
|
+
// Handle WHERE clause
|
|
68
|
+
const whereClause = buildSqliteWhereClause(
|
|
69
|
+
s3q.criteria.where,
|
|
70
|
+
WLModel,
|
|
71
|
+
s3q.meta
|
|
72
|
+
)
|
|
73
|
+
if (whereClause) {
|
|
74
|
+
sqlQuery += ` WHERE ${whereClause}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle SORT clause
|
|
78
|
+
if (s3q.criteria.sort && s3q.criteria.sort.length) {
|
|
79
|
+
const sortClauses = s3q.criteria.sort.map((sortObj) => {
|
|
80
|
+
const key = Object.keys(sortObj)[0]
|
|
81
|
+
const direction = sortObj[key] === 'ASC' ? 'ASC' : 'DESC'
|
|
82
|
+
return `\`${key}\` ${direction}`
|
|
83
|
+
})
|
|
84
|
+
sqlQuery += ` ORDER BY ${sortClauses.join(', ')}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle LIMIT clause
|
|
88
|
+
// If no limit is specified, don't add LIMIT clause (will return all matching records)
|
|
89
|
+
// This handles cases where Waterline doesn't provide a default limit
|
|
90
|
+
if (
|
|
91
|
+
s3q.criteria.limit !== undefined &&
|
|
92
|
+
Number.isFinite(s3q.criteria.limit)
|
|
93
|
+
) {
|
|
94
|
+
sqlQuery += ` LIMIT ${s3q.criteria.limit}`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle SKIP (OFFSET) clause
|
|
98
|
+
if (s3q.criteria.skip) {
|
|
99
|
+
sqlQuery += ` OFFSET ${s3q.criteria.skip}`
|
|
100
|
+
}
|
|
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()
|
|
108
|
+
|
|
109
|
+
// Process records
|
|
110
|
+
const phRecords = nativeResult.map((record) => {
|
|
111
|
+
processNativeRecord(record, WLModel, s3q.meta)
|
|
112
|
+
return record
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return exits.success(phRecords)
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return exits.error(err)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Get connection',
|
|
3
|
+
|
|
4
|
+
description:
|
|
5
|
+
'Get an active connection to the SQLite database (this is a no-op for SQLite).',
|
|
6
|
+
|
|
7
|
+
moreInfoUrl:
|
|
8
|
+
'https://github.com/node-machine/driver-interface/blob/master/machines/get-connection.js',
|
|
9
|
+
|
|
10
|
+
sync: true,
|
|
11
|
+
|
|
12
|
+
inputs: {
|
|
13
|
+
manager: {
|
|
14
|
+
description: 'A SQLite database instance (from better-sqlite3).',
|
|
15
|
+
example: '===',
|
|
16
|
+
required: true
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
meta: {
|
|
20
|
+
friendlyName: 'Meta (unused)',
|
|
21
|
+
description: 'Additional stuff to pass to the driver.',
|
|
22
|
+
example: '==='
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
exits: {
|
|
27
|
+
success: {
|
|
28
|
+
outputFriendlyName: 'Report',
|
|
29
|
+
outputDescription:
|
|
30
|
+
'The `connection` property is a SQLite database instance. The `meta` property is unused.',
|
|
31
|
+
outputExample: '==='
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
failed: {
|
|
35
|
+
friendlyName: 'Failed (unused)',
|
|
36
|
+
description:
|
|
37
|
+
'Could not acquire a connection to the database via the provided connection manager. (This is unlikely to occur with SQLite)',
|
|
38
|
+
outputFriendlyName: 'Report',
|
|
39
|
+
outputExample: {
|
|
40
|
+
error: '===',
|
|
41
|
+
meta: '==='
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
fn: ({ manager, meta }, exits) => {
|
|
47
|
+
// This is a no-op that just sends back the manager and `meta` that were passed in.
|
|
48
|
+
// In SQLite, the "manager" and "connection" are the same thing: a Database instance from better-sqlite3.
|
|
49
|
+
return exits.success({
|
|
50
|
+
connection: manager,
|
|
51
|
+
meta
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Join',
|
|
3
|
+
description: 'Perform a join operation in SQLite using better-sqlite3.',
|
|
4
|
+
inputs: {
|
|
5
|
+
query: require('../constants/query.input'),
|
|
6
|
+
connection: require('../constants/connection.input'),
|
|
7
|
+
dryOrm: require('../constants/dry-orm.input')
|
|
8
|
+
},
|
|
9
|
+
exits: {
|
|
10
|
+
success: {
|
|
11
|
+
description: 'The query was run successfully.',
|
|
12
|
+
outputType: 'ref'
|
|
13
|
+
},
|
|
14
|
+
error: {
|
|
15
|
+
description: 'An error occurred while performing the query.'
|
|
16
|
+
}
|
|
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')
|
|
24
|
+
|
|
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
|
|
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
|
|
118
|
+
|
|
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]
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (strategy === 3) {
|
|
130
|
+
_.each(records, function (record) {
|
|
131
|
+
delete record._parent_fk
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (records.length) {
|
|
136
|
+
cache.records = records
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
queryCache.set(cache)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Set parents
|
|
144
|
+
queryCache.setParents(sortedResults.parents)
|
|
145
|
+
|
|
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)
|
|
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
|
+
)
|
|
258
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildSqliteWhereClause()
|
|
3
|
+
*
|
|
4
|
+
* Build a SQLite WHERE clause from the specified S3Q `where` clause.
|
|
5
|
+
* > Note: The provided `where` clause is NOT mutated.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} whereClause [`where` clause from the criteria of a S3Q]
|
|
8
|
+
* @param {Object} WLModel
|
|
9
|
+
* @param {Object?} meta [`meta` query key from the s3q]
|
|
10
|
+
*
|
|
11
|
+
* @returns {String} [SQLite WHERE clause]
|
|
12
|
+
*/
|
|
13
|
+
module.exports = function buildSqliteWhereClause(whereClause, WLModel, meta) {
|
|
14
|
+
// Handle null, undefined, or empty `where` clause.
|
|
15
|
+
if (!whereClause || Object.keys(whereClause).length === 0) {
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
|
|
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))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return isRoot ? clauses.join(' AND ') : clauses.join(' AND ')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return recurse(whereClause)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildConstraint(columnName, constraint, WLModel, meta) {
|
|
37
|
+
if (typeof constraint !== 'object' || constraint === null) {
|
|
38
|
+
return `${columnName} = ${sqliteEscape(constraint)}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const modifierKind = Object.keys(constraint)[0]
|
|
42
|
+
const modifier = constraint[modifierKind]
|
|
43
|
+
|
|
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}%`
|
|
66
|
+
}
|
|
67
|
+
let clause = `${columnName} LIKE '${likePattern.replace(/'/g, "''")}'`
|
|
68
|
+
if (meta && meta.makeLikeModifierCaseInsensitive === true) {
|
|
69
|
+
clause = `LOWER(${columnName}) LIKE LOWER('${likePattern.replace(/'/g, "''")}')`
|
|
70
|
+
}
|
|
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
|
+
}
|
|
78
|
+
|
|
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'
|
|
86
|
+
}
|
|
87
|
+
if (value === null) {
|
|
88
|
+
return 'NULL'
|
|
89
|
+
}
|
|
90
|
+
return value
|
|
91
|
+
}
|