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,69 @@
1
+ const Machine = require('machine')
2
+
3
+ /**
4
+ * buildStdAdapterMethod()
5
+ *
6
+ * Build a generic DQL/DML adapter method for SQLite from a machine definition and available state.
7
+ *
8
+ * @param {Object} machineDef - The machine definition (dry)
9
+ * @param {Object} registeredDsEntries - Registered datastore entries
10
+ * @param {Object} registeredDryModels - Registered dry models
11
+ * @returns {Function} - The adapter method
12
+ */
13
+ module.exports = function buildStdAdapterMethod(
14
+ machineDef,
15
+ wetMachines,
16
+ registeredDsEntries,
17
+ registeredDryModels
18
+ ) {
19
+ // Build wet machine.
20
+ const performQuery = Machine.build(machineDef)
21
+
22
+ // Return function that will be the adapter method.
23
+ return function (datastoreName, s3q, done) {
24
+ // Look up the datastore entry (to get the manager).
25
+ const dsEntry = registeredDsEntries[datastoreName]
26
+
27
+ // Sanity check:
28
+ if (!dsEntry) {
29
+ return done(
30
+ new Error(
31
+ `Consistency violation: Cannot do that with datastore (${datastoreName}) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at http://sailsjs.com/support.)`
32
+ )
33
+ )
34
+ }
35
+
36
+ // For SQLite, we don't need to obtain a separate connection. The manager is the connection.
37
+ const connection = dsEntry.manager
38
+
39
+ // Build switch handlers based on the machine's defined exits
40
+ const switchHandlers = {
41
+ error: function (err) {
42
+ return done(err)
43
+ },
44
+ success: function (result) {
45
+ return done(null, result)
46
+ }
47
+ }
48
+
49
+ // Only add notUnique handler if the machine defines this exit
50
+ if (machineDef.exits.notUnique) {
51
+ switchHandlers.notUnique = function (errInfo) {
52
+ // Create error in same format as sails-postgresql
53
+ const e = new Error(errInfo.message || 'Not unique')
54
+ e.code = 'E_UNIQUE'
55
+ if (errInfo.footprint) {
56
+ e.footprint = errInfo.footprint
57
+ }
58
+ return done(e)
59
+ }
60
+ }
61
+
62
+ // Perform the query
63
+ performQuery({
64
+ query: s3q,
65
+ connection: connection,
66
+ dryOrm: { models: registeredDryModels }
67
+ }).switch(switchHandlers)
68
+ }
69
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `connection`
3
+ *
4
+ * @constant
5
+ * @type {InputDef}
6
+ */
7
+ module.exports = {
8
+ description: 'The active database connection to use.',
9
+ extendedDescription:
10
+ 'This connection _will not be released automatically_ or mutated in any other way by this machine.',
11
+ whereToGet: { description: 'Use getConnection().' },
12
+ example: '===',
13
+ readOnly: true,
14
+ required: true
15
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `dryOrm`
3
+ *
4
+ * @constant
5
+ * @type {InputDef}
6
+ */
7
+ module.exports = {
8
+ friendlyName: 'Dry ORM',
9
+ description: 'The "dry ORM" instance.',
10
+ extendedDescription:
11
+ 'This includes a property called `models`, which is a dictionary containing all known model definitions, keyed by model identity.',
12
+ required: true,
13
+ readOnly: true,
14
+ example: '==='
15
+ //e.g.
16
+ //```
17
+ //{
18
+ // models: {
19
+ // pet: {attributes:{...}, tableName: 'sack_of_pets', identity: 'pet'},
20
+ // },
21
+ //}
22
+ //```
23
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `meta`
3
+ *
4
+ * @constant
5
+ * @type {InputDef}
6
+ */
7
+ module.exports = {
8
+ friendlyName: 'Meta (custom)',
9
+ description:
10
+ "A dictionary of additional options to customize this behavior. (e.g. `{foo: 'bar'}`)",
11
+ moreInfoUrl:
12
+ 'https://github.com/node-machine/driver-interface/blob/3f3a150ef4ece40dc0d105006e2766e81af23719/constants/meta.input.js',
13
+ example: '==='
14
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `notUnique`
3
+ *
4
+ * @constant
5
+ * @type {ExitDef}
6
+ */
7
+ module.exports = {
8
+ description:
9
+ 'Could not persist changes because they would violate one or more uniqueness constraints.',
10
+ moreInfoUrl:
11
+ 'https://github.com/sailshq/waterline-query-docs/blob/8fc158d8460aa04ee6233fefbdf83cc17e7645df/docs/errors.md',
12
+ outputFriendlyName: 'Uniqueness error',
13
+ outputDescription:
14
+ 'A native error from the database, with an extra key (`footprint`) attached.',
15
+ outputExample: '===' // e.g. an Error instance with a `footprint` attached
16
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `query`
3
+ *
4
+ * @constant
5
+ * @type {InputDef}
6
+ */
7
+ module.exports = {
8
+ friendlyName: 'Query (s3q)',
9
+ description: 'A stage three Waterline query.',
10
+ extendedDescription:
11
+ 'The `meta` key of this dictionary is reserved for certain special "meta keys" (e.g. flags, signals, etc.) and other custom, adapter-specific extensions.',
12
+ required: true,
13
+ readOnly: true,
14
+ example: '===' //e.g. `{ method: 'create', using: 'the_table_name', ... }`
15
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `tableName`
3
+ *
4
+ * @constant
5
+ * @type {InputDef}
6
+ */
7
+ module.exports = {
8
+ friendlyName: 'Table name',
9
+ description:
10
+ 'The name of the physical model (i.e. the name of the Mongo collection -- aka "tableName").',
11
+ example: 'foo_bar'
12
+ }
@@ -0,0 +1,74 @@
1
+ const buildSqliteWhereClause = require('./private/build-sqlite-where-clause')
2
+
3
+ module.exports = {
4
+ friendlyName: 'Avg (records)',
5
+
6
+ description: 'Return the Average of the records matched by the query.',
7
+
8
+ inputs: {
9
+ query: require('../constants/query.input'),
10
+ connection: require('../constants/connection.input'),
11
+ dryOrm: require('../constants/dry-orm.input')
12
+ },
13
+
14
+ exits: {
15
+ success: {
16
+ outputFriendlyName: 'Average (mean)',
17
+ outputDescription:
18
+ 'The average value of the given property across all records.',
19
+ outputExample: -48.1293
20
+ }
21
+ },
22
+
23
+ fn: function (inputs, exits) {
24
+ const s3q = inputs.query
25
+
26
+ const tableName = s3q.using
27
+ const numericFieldName = s3q.numericAttrName
28
+
29
+ // Grab the model definition
30
+ // Find model by tableName since models is an object, not an array
31
+ let WLModel = null
32
+ for (const modelIdentity in inputs.dryOrm.models) {
33
+ if (inputs.dryOrm.models[modelIdentity].tableName === tableName) {
34
+ WLModel = inputs.dryOrm.models[modelIdentity]
35
+ break
36
+ }
37
+ }
38
+ if (!WLModel) {
39
+ return exits.error(
40
+ new Error(
41
+ `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.)`
42
+ )
43
+ )
44
+ }
45
+
46
+ // Build a SQLite WHERE clause from the `where` clause.
47
+ let whereClause
48
+ try {
49
+ whereClause = buildSqliteWhereClause(
50
+ s3q.criteria.where,
51
+ WLModel,
52
+ s3q.meta
53
+ )
54
+ } catch (e) {
55
+ return exits.error(e)
56
+ }
57
+
58
+ const db = inputs.connection
59
+
60
+ try {
61
+ let avgQuery = `SELECT COALESCE(AVG(\`${numericFieldName}\`), 0) as average FROM \`${tableName}\``
62
+ if (whereClause) {
63
+ avgQuery += ` WHERE ${whereClause}`
64
+ }
65
+
66
+ const stmt = db.prepare(avgQuery)
67
+ const result = stmt.get()
68
+
69
+ return exits.success(result.average)
70
+ } catch (err) {
71
+ return exits.error(err)
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Module dependencies
3
+ */
4
+
5
+ /**
6
+ * Begin Transaction
7
+ *
8
+ * Begin a new database transaction on the provided connection.
9
+ */
10
+
11
+ module.exports = {
12
+ friendlyName: 'Begin transaction',
13
+
14
+ description: 'Begin a new database transaction on the provided connection.',
15
+
16
+ moreInfoUrl:
17
+ 'https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#transactionfunction---function',
18
+
19
+ inputs: {
20
+ connection: {
21
+ description:
22
+ 'An active database connection that was acquired from a manager.',
23
+ example: '===',
24
+ required: true
25
+ },
26
+
27
+ meta: {
28
+ description: 'Additional options for this query.',
29
+ example: '==='
30
+ }
31
+ },
32
+
33
+ fn: function beginTransaction(inputs, exits) {
34
+ const db = inputs.connection
35
+ const meta = inputs.meta || {}
36
+
37
+ try {
38
+ if (db.inTransaction) {
39
+ return exits.error(
40
+ new Error('Transaction is already active on this connection.')
41
+ )
42
+ }
43
+
44
+ db.prepare('BEGIN TRANSACTION').run()
45
+
46
+ return exits.success()
47
+ } catch (err) {
48
+ return exits.error(err)
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Module dependencies
3
+ */
4
+
5
+ /**
6
+ * Commit Transaction
7
+ *
8
+ * Commit the current database transaction on the provided connection.
9
+ */
10
+
11
+ module.exports = {
12
+ friendlyName: 'Commit transaction',
13
+
14
+ description:
15
+ 'Commit the current database transaction on the provided connection.',
16
+
17
+ moreInfoUrl:
18
+ 'https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#transactionfunction---function',
19
+
20
+ inputs: {
21
+ connection: {
22
+ description:
23
+ 'An active database connection that was acquired from a manager.',
24
+ example: '===',
25
+ required: true
26
+ },
27
+
28
+ meta: {
29
+ description: 'Additional options for this query.',
30
+ example: '==='
31
+ }
32
+ },
33
+
34
+ fn: function commitTransaction(inputs, exits) {
35
+ const db = inputs.connection
36
+ const meta = inputs.meta || {}
37
+
38
+ try {
39
+ if (!db.inTransaction) {
40
+ return exits.error(new Error('No active transaction to commit.'))
41
+ }
42
+
43
+ db.prepare('COMMIT TRANSACTION').run()
44
+
45
+ return exits.success()
46
+ } catch (err) {
47
+ return exits.error(err)
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,78 @@
1
+ const buildSqliteWhereClause = require('./private/build-sqlite-where-clause')
2
+
3
+ module.exports = {
4
+ friendlyName: 'Count (records)',
5
+
6
+ description: 'Return the count of the records matched by the query.',
7
+
8
+ inputs: {
9
+ query: require('../constants/query.input'),
10
+ connection: require('../constants/connection.input'),
11
+ dryOrm: require('../constants/dry-orm.input')
12
+ },
13
+
14
+ exits: {
15
+ success: {
16
+ outputFriendlyName: 'Total (# of records)',
17
+ outputDescription: 'The number of matching records.',
18
+ outputExample: 59
19
+ }
20
+ },
21
+
22
+ fn: function (inputs, exits) {
23
+ const s3q = inputs.query
24
+ if (s3q.meta && s3q.meta.logSqliteS3Qs) {
25
+ console.log(
26
+ '* * * * * *\nADAPTER (COUNT RECORDS):',
27
+ require('util').inspect(s3q, { depth: 5 }),
28
+ '\n'
29
+ )
30
+ }
31
+
32
+ const tableName = s3q.using
33
+
34
+ // Find model by tableName since models is an object, not an array
35
+ let WLModel = null
36
+ for (const modelIdentity in inputs.dryOrm.models) {
37
+ if (inputs.dryOrm.models[modelIdentity].tableName === tableName) {
38
+ WLModel = inputs.dryOrm.models[modelIdentity]
39
+ break
40
+ }
41
+ }
42
+ if (!WLModel) {
43
+ return exits.error(
44
+ new Error(
45
+ `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.)`
46
+ )
47
+ )
48
+ }
49
+
50
+ // Build a SQLite WHERE clause from the `where` clause.
51
+ let whereClause
52
+ try {
53
+ whereClause = buildSqliteWhereClause(
54
+ s3q.criteria.where,
55
+ WLModel,
56
+ s3q.meta
57
+ )
58
+ } catch (e) {
59
+ return exits.error(e)
60
+ }
61
+
62
+ const db = inputs.connection
63
+
64
+ try {
65
+ let countQuery = `SELECT COUNT(*) as count FROM \`${tableName}\``
66
+ if (whereClause) {
67
+ countQuery += ` WHERE ${whereClause}`
68
+ }
69
+
70
+ const stmt = db.prepare(countQuery)
71
+ const result = stmt.get()
72
+
73
+ return exits.success(result.count)
74
+ } catch (err) {
75
+ return exits.error(err)
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,163 @@
1
+ const util = require('util')
2
+ const processNativeRecord = require('./private/process-native-record')
3
+ const processNativeError = require('./private/process-native-error')
4
+ const reifyValuesToSet = require('./private/reify-values-to-set')
5
+
6
+ module.exports = {
7
+ friendlyName: 'Create each (record)',
8
+
9
+ description: 'Insert multiple records into a table 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 (maybe)',
20
+ outputDescription:
21
+ 'Either `null` or (if `fetch:true`) an array of new physical records that were created.',
22
+ outputExample: '==='
23
+ },
24
+ notUnique: require('../constants/not-unique.exit')
25
+ },
26
+
27
+ fn: async function (inputs, exits) {
28
+ const s3q = inputs.query
29
+ if (s3q.meta && s3q.meta.logSQLiteS3Qs) {
30
+ console.log(
31
+ '* * * * * *\nADAPTER (CREATE EACH RECORD):',
32
+ util.inspect(s3q, { depth: 5 }),
33
+ '\n'
34
+ )
35
+ }
36
+
37
+ const tableName = s3q.using
38
+ // Find model by tableName since models is an object, not an array
39
+ let WLModel = null
40
+ for (const modelIdentity in inputs.dryOrm.models) {
41
+ if (inputs.dryOrm.models[modelIdentity].tableName === tableName) {
42
+ WLModel = inputs.dryOrm.models[modelIdentity]
43
+ break
44
+ }
45
+ }
46
+
47
+ if (!WLModel) {
48
+ return exits.error(
49
+ new Error(
50
+ `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.)`
51
+ )
52
+ )
53
+ }
54
+
55
+ try {
56
+ s3q.newRecords.forEach((newRecord) => {
57
+ reifyValuesToSet(newRecord, WLModel, s3q.meta)
58
+ })
59
+ } catch (e) {
60
+ return exits.error(e)
61
+ }
62
+
63
+ const isFetchEnabled = !!(s3q.meta && s3q.meta.fetch)
64
+
65
+ const db = inputs.connection
66
+
67
+ // Validate records array
68
+ if (!Array.isArray(s3q.newRecords) || s3q.newRecords.length === 0) {
69
+ return exits.error(
70
+ new Error(
71
+ 'Cannot create records: no data provided or invalid data format'
72
+ )
73
+ )
74
+ }
75
+
76
+ try {
77
+ // Performance optimization: Use a single INSERT statement with multiple VALUES
78
+ // This is much more efficient than individual INSERT statements
79
+ const firstRecord = s3q.newRecords[0]
80
+ const columnNames = Object.keys(firstRecord)
81
+
82
+ // Validate that all records have the same columns
83
+ const invalidRecord = s3q.newRecords.find((record) => {
84
+ const recordColumns = Object.keys(record)
85
+ return (
86
+ recordColumns.length !== columnNames.length ||
87
+ !recordColumns.every((col) => columnNames.includes(col))
88
+ )
89
+ })
90
+
91
+ if (invalidRecord) {
92
+ throw new Error(
93
+ 'All records must have the same columns for batch insert'
94
+ )
95
+ }
96
+
97
+ const columns = columnNames.map((col) => `\`${col}\``).join(', ')
98
+ const valueClause = `(${columnNames.map(() => '?').join(', ')})`
99
+ const allValueClauses = Array(s3q.newRecords.length)
100
+ .fill(valueClause)
101
+ .join(', ')
102
+ const sql = `INSERT INTO \`${tableName}\` (${columns}) VALUES ${allValueClauses}`
103
+
104
+ // Flatten all values for the batch insert
105
+ const allValues = s3q.newRecords.flatMap((record) =>
106
+ columnNames.map((col) => record[col])
107
+ )
108
+
109
+ // Use transaction for atomic batch insert - recommended for performance
110
+ let insertInfo
111
+ if (db.runInTransaction) {
112
+ insertInfo = db.runInTransaction(() => {
113
+ const stmt = db.getPreparedStatement
114
+ ? db.getPreparedStatement(sql)
115
+ : db.prepare(sql)
116
+ return stmt.run(allValues)
117
+ })
118
+ } else {
119
+ // Fallback transaction approach
120
+ const transaction = db.transaction(() => {
121
+ const stmt = db.prepare(sql)
122
+ return stmt.run(allValues)
123
+ })
124
+ insertInfo = transaction()
125
+ }
126
+
127
+ // If `fetch` is NOT enabled, we're done.
128
+ if (!isFetchEnabled) {
129
+ return exits.success()
130
+ }
131
+
132
+ // For batch inserts, we need to calculate the range of inserted IDs
133
+ // SQLite auto-increments IDs sequentially in a transaction
134
+ const lastInsertRowid = insertInfo.lastInsertRowid
135
+ const recordCount = s3q.newRecords.length
136
+ const firstInsertRowid = lastInsertRowid - recordCount + 1
137
+
138
+ // Fetch the inserted records using the ID range
139
+ const selectSql = `SELECT * FROM \`${tableName}\` WHERE rowid >= ? AND rowid <= ? ORDER BY rowid`
140
+ const selectStmt = db.prepare(selectSql)
141
+ const phRecords = selectStmt.all(firstInsertRowid, lastInsertRowid)
142
+
143
+ if (phRecords.length !== recordCount) {
144
+ throw new Error(
145
+ `Consistency violation: Expected ${recordCount} records but retrieved ${phRecords.length}`
146
+ )
147
+ }
148
+
149
+ // Process records in place for better performance
150
+ phRecords.forEach((phRecord) => {
151
+ processNativeRecord(phRecord, WLModel, s3q.meta)
152
+ })
153
+
154
+ return exits.success(phRecords)
155
+ } catch (err) {
156
+ err = processNativeError(err)
157
+ if (err.footprint && err.footprint.identity === 'notUnique') {
158
+ return exits.notUnique(err)
159
+ }
160
+ return exits.error(err)
161
+ }
162
+ }
163
+ }