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.
Files changed (48) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/workflows/prettier.yml +16 -0
  3. package/.github/workflows/test.yml +16 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.prettierrc.js +5 -0
  6. package/CHANGELOG.md +161 -0
  7. package/LICENSE +21 -0
  8. package/README.md +247 -0
  9. package/lib/index.js +1104 -0
  10. package/lib/private/build-std-adapter-method.js +69 -0
  11. package/lib/private/constants/connection.input.js +15 -0
  12. package/lib/private/constants/dry-orm.input.js +23 -0
  13. package/lib/private/constants/meta.input.js +14 -0
  14. package/lib/private/constants/not-unique.exit.js +16 -0
  15. package/lib/private/constants/query.input.js +15 -0
  16. package/lib/private/constants/table-name.input.js +12 -0
  17. package/lib/private/machines/avg-records.js +74 -0
  18. package/lib/private/machines/begin-transaction.js +51 -0
  19. package/lib/private/machines/commit-transaction.js +50 -0
  20. package/lib/private/machines/count-records.js +78 -0
  21. package/lib/private/machines/create-each-record.js +163 -0
  22. package/lib/private/machines/create-manager.js +174 -0
  23. package/lib/private/machines/create-record.js +126 -0
  24. package/lib/private/machines/define-physical-model.js +111 -0
  25. package/lib/private/machines/destroy-manager.js +87 -0
  26. package/lib/private/machines/destroy-records.js +114 -0
  27. package/lib/private/machines/drop-physical-model.js +51 -0
  28. package/lib/private/machines/find-records.js +120 -0
  29. package/lib/private/machines/get-connection.js +54 -0
  30. package/lib/private/machines/join.js +259 -0
  31. package/lib/private/machines/lease-connection.js +58 -0
  32. package/lib/private/machines/private/build-sqlite-where-clause.js +91 -0
  33. package/lib/private/machines/private/compile-statement.js +334 -0
  34. package/lib/private/machines/private/generate-join-sql-query.js +385 -0
  35. package/lib/private/machines/private/process-each-record.js +106 -0
  36. package/lib/private/machines/private/process-native-error.js +104 -0
  37. package/lib/private/machines/private/process-native-record.js +104 -0
  38. package/lib/private/machines/private/reify-values-to-set.js +83 -0
  39. package/lib/private/machines/release-connection.js +70 -0
  40. package/lib/private/machines/rollback-transaction.js +50 -0
  41. package/lib/private/machines/set-physical-sequence.js +77 -0
  42. package/lib/private/machines/sum-records.js +75 -0
  43. package/lib/private/machines/update-records.js +162 -0
  44. package/lib/private/machines/verify-model-def.js +38 -0
  45. package/package.json +58 -5
  46. package/tests/index.js +88 -0
  47. package/tests/runner.js +99 -0
  48. 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
+ }