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.
Files changed (44) 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 +928 -0
  10. package/lib/private/build-std-adapter-method.js +65 -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/count-records.js +78 -0
  19. package/lib/private/machines/create-each-record.js +163 -0
  20. package/lib/private/machines/create-manager.js +174 -0
  21. package/lib/private/machines/create-record.js +126 -0
  22. package/lib/private/machines/define-physical-model.js +102 -0
  23. package/lib/private/machines/destroy-manager.js +87 -0
  24. package/lib/private/machines/destroy-records.js +101 -0
  25. package/lib/private/machines/drop-physical-model.js +51 -0
  26. package/lib/private/machines/find-records.js +120 -0
  27. package/lib/private/machines/get-connection.js +54 -0
  28. package/lib/private/machines/join.js +93 -0
  29. package/lib/private/machines/private/build-sqlite-where-clause.js +89 -0
  30. package/lib/private/machines/private/generate-join-sql-query.js +377 -0
  31. package/lib/private/machines/private/process-each-record.js +99 -0
  32. package/lib/private/machines/private/process-native-error.js +59 -0
  33. package/lib/private/machines/private/process-native-record.js +104 -0
  34. package/lib/private/machines/private/reify-values-to-set.js +83 -0
  35. package/lib/private/machines/release-connection.js +70 -0
  36. package/lib/private/machines/set-physical-sequence.js +77 -0
  37. package/lib/private/machines/sum-records.js +75 -0
  38. package/lib/private/machines/update-records.js +145 -0
  39. package/lib/private/machines/verify-model-def.js +38 -0
  40. package/package.json +53 -5
  41. package/tests/adapter.test.js +534 -0
  42. package/tests/datatypes.test.js +293 -0
  43. package/tests/index.js +88 -0
  44. 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,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,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
+ }