sails-sqlite 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/lib/index.js +204 -28
  2. package/lib/private/build-std-adapter-method.js +8 -4
  3. package/lib/private/machines/avg-records.js +1 -1
  4. package/lib/private/machines/begin-transaction.js +51 -0
  5. package/lib/private/machines/commit-transaction.js +50 -0
  6. package/lib/private/machines/count-records.js +1 -1
  7. package/lib/private/machines/create-each-record.js +1 -1
  8. package/lib/private/machines/create-record.js +2 -2
  9. package/lib/private/machines/define-physical-model.js +18 -9
  10. package/lib/private/machines/destroy-records.js +21 -8
  11. package/lib/private/machines/drop-physical-model.js +2 -2
  12. package/lib/private/machines/find-records.js +3 -3
  13. package/lib/private/machines/join.js +232 -66
  14. package/lib/private/machines/lease-connection.js +58 -0
  15. package/lib/private/machines/private/build-sqlite-where-clause.js +10 -8
  16. package/lib/private/machines/private/compile-statement.js +334 -0
  17. package/lib/private/machines/private/generate-join-sql-query.js +14 -6
  18. package/lib/private/machines/private/process-each-record.js +9 -2
  19. package/lib/private/machines/private/process-native-error.js +85 -40
  20. package/lib/private/machines/rollback-transaction.js +50 -0
  21. package/lib/private/machines/sum-records.js +1 -1
  22. package/lib/private/machines/update-records.js +27 -10
  23. package/package.json +8 -3
  24. package/tests/index.js +1 -1
  25. package/tests/runner.js +99 -0
  26. package/tests/transaction.test.js +562 -0
  27. package/tests/adapter.test.js +0 -534
  28. package/tests/datatypes.test.js +0 -293
  29. package/tests/sequence.test.js +0 -153
@@ -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
- datastore: {
11
- description: 'The datastore to use for the query.',
12
- required: true,
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
- fn: async function (inputs, exits) {
39
- const { datastore, models, query } = inputs
40
-
41
- try {
42
- const { joins } = require('waterline-utils')
43
-
44
- // Convert join criteria using waterline-utils
45
- const joinCriteria = joins.convertJoinCriteria({
46
- query,
47
- getPk: (tableName) => {
48
- // Find the model by tableName
49
- let targetModel = null
50
- for (const modelIdentity in models) {
51
- if (models[modelIdentity].tableName === tableName) {
52
- targetModel = models[modelIdentity]
53
- break
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
- if (!targetModel) {
58
- throw new Error(`No model found with tableName: ${tableName}`)
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
- const pkAttrName = targetModel.primaryKey
62
- const pkDef = targetModel.attributes[pkAttrName]
63
- return pkDef.columnName || pkAttrName
129
+ if (strategy === 3) {
130
+ _.each(records, function (record) {
131
+ delete record._parent_fk
132
+ })
64
133
  }
65
- })
66
134
 
67
- // Generate SQL query using our helper
68
- const { sql, bindings } = generateJoinSqlQuery(
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
- return exits.success(processedResults)
89
- } catch (error) {
90
- return exits.error(error)
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
- .replace(/^%/, '.*')
62
- .replace(/([^\\])%/g, '$1.*')
63
- .replace(/\\%/g, '%')
64
- likePattern = `^${likePattern}$`
65
- let clause = `${columnName} REGEXP '${likePattern}'`
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}) REGEXP '${likePattern.toLowerCase()}'`
69
+ clause = `LOWER(${columnName}) LIKE LOWER('${likePattern.replace(/'/g, "''")}')`
68
70
  }
69
71
  return clause
70
72
  default: