sails-sqlite 0.0.0 → 0.1.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 +928 -0
- package/lib/private/build-std-adapter-method.js +65 -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/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 +102 -0
- package/lib/private/machines/destroy-manager.js +87 -0
- package/lib/private/machines/destroy-records.js +101 -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 +93 -0
- package/lib/private/machines/private/build-sqlite-where-clause.js +89 -0
- package/lib/private/machines/private/generate-join-sql-query.js +377 -0
- package/lib/private/machines/private/process-each-record.js +99 -0
- package/lib/private/machines/private/process-native-error.js +59 -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/set-physical-sequence.js +77 -0
- package/lib/private/machines/sum-records.js +75 -0
- package/lib/private/machines/update-records.js +145 -0
- package/lib/private/machines/verify-model-def.js +38 -0
- package/package.json +53 -5
- package/tests/adapter.test.js +534 -0
- package/tests/datatypes.test.js +293 -0
- package/tests/index.js +88 -0
- package/tests/sequence.test.js +153 -0
|
@@ -0,0 +1,65 @@
|
|
|
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 (err) {
|
|
52
|
+
return done(
|
|
53
|
+
Object.assign(new Error('Not unique'), { code: 'E_UNIQUE' })
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Perform the query
|
|
59
|
+
performQuery({
|
|
60
|
+
query: s3q,
|
|
61
|
+
connection: connection,
|
|
62
|
+
dryOrm: { models: registeredDryModels }
|
|
63
|
+
}).switch(switchHandlers)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -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,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,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.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
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Create manager',
|
|
3
|
+
|
|
4
|
+
description: 'Build and initialize a connection manager instance for SQLite.',
|
|
5
|
+
|
|
6
|
+
inputs: {
|
|
7
|
+
connectionString: {
|
|
8
|
+
description: 'The SQLite connection string (file path).',
|
|
9
|
+
example: 'db/database.sqlite',
|
|
10
|
+
required: true
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
meta: {
|
|
14
|
+
friendlyName: 'Meta (custom)',
|
|
15
|
+
description:
|
|
16
|
+
'A dictionary of additional options to pass in when instantiating the SQLite client.',
|
|
17
|
+
example: '==='
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
exits: {
|
|
22
|
+
success: {
|
|
23
|
+
description: 'Connected to SQLite successfully.',
|
|
24
|
+
outputFriendlyName: 'Report',
|
|
25
|
+
outputDescription:
|
|
26
|
+
'The `manager` property is a SQLite database instance.',
|
|
27
|
+
outputExample: '==='
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
fn: function ({ connectionString, meta }, exits) {
|
|
32
|
+
const Database = require('better-sqlite3')
|
|
33
|
+
const path = require('path')
|
|
34
|
+
const fs = require('fs')
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Ensure the directory exists for the database file
|
|
38
|
+
const dbDir = path.dirname(connectionString)
|
|
39
|
+
if (dbDir !== '.' && !fs.existsSync(dbDir)) {
|
|
40
|
+
fs.mkdirSync(dbDir, { recursive: true })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create database connection with optimized options
|
|
44
|
+
const dbOptions = {
|
|
45
|
+
// Enable verbose mode in development
|
|
46
|
+
verbose:
|
|
47
|
+
meta?.verbose || process.env.NODE_ENV === 'development'
|
|
48
|
+
? console.log
|
|
49
|
+
: null,
|
|
50
|
+
// Set timeout for database operations
|
|
51
|
+
timeout: meta?.timeout || 5000,
|
|
52
|
+
// Enable read-only mode if specified
|
|
53
|
+
readonly: meta?.readonly || false,
|
|
54
|
+
// Enable file must exist mode if specified
|
|
55
|
+
fileMustExist: meta?.fileMustExist || false,
|
|
56
|
+
...meta
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const db = new Database(connectionString, dbOptions)
|
|
60
|
+
|
|
61
|
+
// Apply recommended performance pragmas for optimal SQLite performance
|
|
62
|
+
const defaultPragmas = {
|
|
63
|
+
// WAL mode for better concurrency (default)
|
|
64
|
+
journal_mode: 'WAL',
|
|
65
|
+
// Synchronous mode for better performance vs durability balance
|
|
66
|
+
synchronous: 'NORMAL',
|
|
67
|
+
// Enable foreign key support
|
|
68
|
+
foreign_keys: 'ON',
|
|
69
|
+
// Set cache size to 256MB (negative value means KB)
|
|
70
|
+
cache_size: -262144,
|
|
71
|
+
// Set page size to 4KB (recommended for modern systems)
|
|
72
|
+
page_size: 4096,
|
|
73
|
+
// Optimize for read-heavy workloads
|
|
74
|
+
optimize: true,
|
|
75
|
+
// Enable memory-mapped I/O
|
|
76
|
+
mmap_size: 268435456, // 256MB
|
|
77
|
+
// Set busy timeout to 30 seconds
|
|
78
|
+
busy_timeout: 30000,
|
|
79
|
+
// Enable automatic index creation for WHERE clauses
|
|
80
|
+
automatic_index: 'ON',
|
|
81
|
+
// Optimize temp store for performance
|
|
82
|
+
temp_store: 'MEMORY'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Merge with user-provided pragmas
|
|
86
|
+
const pragmas = { ...defaultPragmas, ...(meta?.pragmas || {}) }
|
|
87
|
+
|
|
88
|
+
// Apply pragmas with error handling
|
|
89
|
+
Object.entries(pragmas).forEach(([key, value]) => {
|
|
90
|
+
if (value !== false && value !== null && value !== undefined) {
|
|
91
|
+
try {
|
|
92
|
+
db.pragma(`${key} = ${value}`)
|
|
93
|
+
} catch (pragmaError) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`Warning: Could not set pragma ${key} = ${value}:`,
|
|
96
|
+
pragmaError.message
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Run ANALYZE to update query planner statistics
|
|
103
|
+
// This is especially important for new databases
|
|
104
|
+
try {
|
|
105
|
+
db.exec('ANALYZE')
|
|
106
|
+
} catch (analyzeError) {
|
|
107
|
+
// ANALYZE might fail on empty database, which is fine
|
|
108
|
+
console.debug(
|
|
109
|
+
'ANALYZE command failed (this is normal for new databases):',
|
|
110
|
+
analyzeError.message
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Prepare commonly used statements for better performance
|
|
115
|
+
// These will be cached and reused throughout the application lifecycle
|
|
116
|
+
const preparedStatements = new Map()
|
|
117
|
+
|
|
118
|
+
// Add helper method to get or create prepared statements
|
|
119
|
+
db.getPreparedStatement = function (sql) {
|
|
120
|
+
if (!preparedStatements.has(sql)) {
|
|
121
|
+
preparedStatements.set(sql, this.prepare(sql))
|
|
122
|
+
}
|
|
123
|
+
return preparedStatements.get(sql)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add transaction helper methods for better performance
|
|
127
|
+
db.runInTransaction = function (fn) {
|
|
128
|
+
return this.transaction(fn)()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add method to optimize database
|
|
132
|
+
db.optimize = function () {
|
|
133
|
+
this.exec('PRAGMA optimize')
|
|
134
|
+
this.exec('VACUUM')
|
|
135
|
+
this.exec('ANALYZE')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Add graceful cleanup method
|
|
139
|
+
db.closeGracefully = function () {
|
|
140
|
+
// Clear prepared statements - newer better-sqlite3 doesn't need explicit finalize
|
|
141
|
+
preparedStatements.clear()
|
|
142
|
+
|
|
143
|
+
// Close the database connection
|
|
144
|
+
if (this.open) {
|
|
145
|
+
this.close()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Set up connection health check
|
|
150
|
+
db.isHealthy = function () {
|
|
151
|
+
try {
|
|
152
|
+
this.prepare('SELECT 1').get()
|
|
153
|
+
return true
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return exits.success({
|
|
160
|
+
manager: db,
|
|
161
|
+
meta: {
|
|
162
|
+
...meta,
|
|
163
|
+
connectionString,
|
|
164
|
+
pragmasApplied: pragmas,
|
|
165
|
+
connectionEstablishedAt: new Date().toISOString()
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return exits.error(
|
|
170
|
+
new Error(`Failed to create SQLite database manager: ${error.message}`)
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|