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.
- 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 +1104 -0
- package/lib/private/build-std-adapter-method.js +69 -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/begin-transaction.js +51 -0
- package/lib/private/machines/commit-transaction.js +50 -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 +111 -0
- package/lib/private/machines/destroy-manager.js +87 -0
- package/lib/private/machines/destroy-records.js +114 -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 +259 -0
- package/lib/private/machines/lease-connection.js +58 -0
- package/lib/private/machines/private/build-sqlite-where-clause.js +91 -0
- package/lib/private/machines/private/compile-statement.js +334 -0
- package/lib/private/machines/private/generate-join-sql-query.js +385 -0
- package/lib/private/machines/private/process-each-record.js +106 -0
- package/lib/private/machines/private/process-native-error.js +104 -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/rollback-transaction.js +50 -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 +162 -0
- package/lib/private/machines/verify-model-def.js +38 -0
- package/package.json +58 -5
- package/tests/index.js +88 -0
- package/tests/runner.js +99 -0
- package/tests/transaction.test.js +562 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Create (record)',
|
|
3
|
+
|
|
4
|
+
description: 'Create a new physical record in the SQLite database.',
|
|
5
|
+
|
|
6
|
+
inputs: {
|
|
7
|
+
query: require('../constants/query.input'),
|
|
8
|
+
connection: require('../constants/connection.input'),
|
|
9
|
+
dryOrm: require('../constants/dry-orm.input')
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
exits: {
|
|
13
|
+
success: {
|
|
14
|
+
outputFriendlyName: 'Record (maybe)',
|
|
15
|
+
outputDescription:
|
|
16
|
+
'Either `null` or (if `fetch:true`) a dictionary representing the new record that was created.',
|
|
17
|
+
outputExample: '==='
|
|
18
|
+
},
|
|
19
|
+
notUnique: require('../constants/not-unique.exit')
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
fn: function (inputs, exits) {
|
|
23
|
+
// Dependencies
|
|
24
|
+
const util = require('util')
|
|
25
|
+
const _ = require('@sailshq/lodash')
|
|
26
|
+
const processNativeRecord = require('./private/process-native-record')
|
|
27
|
+
const processNativeError = require('./private/process-native-error')
|
|
28
|
+
const reifyValuesToSet = require('./private/reify-values-to-set')
|
|
29
|
+
|
|
30
|
+
// Local var for the stage 3 query, for easier access.
|
|
31
|
+
const s3q = inputs.query
|
|
32
|
+
if (s3q.meta && s3q.meta.logSQLiteS3Qs) {
|
|
33
|
+
console.log(
|
|
34
|
+
'* * * * * *\nADAPTER (CREATE RECORD):',
|
|
35
|
+
util.inspect(s3q, { depth: 5 }),
|
|
36
|
+
'\n'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Local var for the `tableName`, for clarity.
|
|
41
|
+
const tableName = s3q.using
|
|
42
|
+
|
|
43
|
+
// Grab the model definition
|
|
44
|
+
const WLModel = _.find(inputs.dryOrm.models, { tableName: tableName })
|
|
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
|
+
// Reify values to set
|
|
54
|
+
try {
|
|
55
|
+
reifyValuesToSet(s3q.newRecord, WLModel, s3q.meta)
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return exits.error(e)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Determine whether to fetch or not
|
|
61
|
+
const isFetchEnabled = !!(s3q.meta && s3q.meta.fetch)
|
|
62
|
+
|
|
63
|
+
// Create this new record in the SQLite database
|
|
64
|
+
const db = inputs.connection
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Build column names and values arrays
|
|
68
|
+
const columnNames = Object.keys(s3q.newRecord)
|
|
69
|
+
const columnValues = Object.values(s3q.newRecord)
|
|
70
|
+
|
|
71
|
+
// Validate that we have data to insert
|
|
72
|
+
if (columnNames.length === 0) {
|
|
73
|
+
throw new Error('Cannot create record: no data provided')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Prepare the INSERT statement with proper SQL escaping
|
|
77
|
+
const columns = columnNames.map((col) => `\`${col}\``).join(', ')
|
|
78
|
+
const placeholders = columnNames.map(() => '?').join(', ')
|
|
79
|
+
const sql = `INSERT INTO \`${tableName}\` (${columns}) VALUES (${placeholders})`
|
|
80
|
+
|
|
81
|
+
// Use prepared statement (optimized for repeated use)
|
|
82
|
+
const stmt = db.getPreparedStatement
|
|
83
|
+
? db.getPreparedStatement(sql)
|
|
84
|
+
: db.prepare(sql)
|
|
85
|
+
|
|
86
|
+
// Execute the INSERT statement within a transaction for consistency
|
|
87
|
+
const info = db.runInTransaction
|
|
88
|
+
? db.runInTransaction(() => stmt.run(columnValues))
|
|
89
|
+
: stmt.run(columnValues)
|
|
90
|
+
|
|
91
|
+
// If `fetch` is NOT enabled, we're done.
|
|
92
|
+
if (!isFetchEnabled) {
|
|
93
|
+
return exits.success()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Otherwise, fetch the newly created record
|
|
97
|
+
const selectSql = `SELECT * FROM \`${tableName}\` WHERE rowid = ?`
|
|
98
|
+
const selectStmt = db.prepare(selectSql)
|
|
99
|
+
const phRecord = selectStmt.get(info.lastInsertRowid)
|
|
100
|
+
|
|
101
|
+
if (!phRecord) {
|
|
102
|
+
return exits.error(
|
|
103
|
+
new Error(
|
|
104
|
+
'Consistency violation: Unable to retrieve the inserted record. This might indicate a consistency violation.'
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Process record (mutate in-place) to wash away adapter-specific eccentricities.
|
|
111
|
+
processNativeRecord(phRecord, WLModel, s3q.meta)
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return exits.error(e)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Send back the record
|
|
117
|
+
return exits.success(phRecord)
|
|
118
|
+
} catch (err) {
|
|
119
|
+
err = processNativeError(err)
|
|
120
|
+
if (err.footprint && err.footprint.identity === 'notUnique') {
|
|
121
|
+
return exits.notUnique(err)
|
|
122
|
+
}
|
|
123
|
+
return exits.error(err)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Define (physical model)',
|
|
3
|
+
|
|
4
|
+
description:
|
|
5
|
+
'Define a physical model (i.e. SQLite table) with the specified characteristics, creating indexes as needed.',
|
|
6
|
+
|
|
7
|
+
sideEffects: 'idempotent',
|
|
8
|
+
|
|
9
|
+
inputs: {
|
|
10
|
+
connection: require('../constants/connection.input'),
|
|
11
|
+
tableName: require('../constants/table-name.input'),
|
|
12
|
+
columns: {
|
|
13
|
+
description: 'An array of column definitions.',
|
|
14
|
+
required: true,
|
|
15
|
+
example: '==='
|
|
16
|
+
},
|
|
17
|
+
meta: require('../constants/meta.input')
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
exits: {
|
|
21
|
+
success: {
|
|
22
|
+
description:
|
|
23
|
+
'New physical model (and any necessary indexes) were created successfully.'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
fn: function (inputs, exits) {
|
|
28
|
+
const db = inputs.connection
|
|
29
|
+
function getSqliteType(columnType) {
|
|
30
|
+
if (!columnType || typeof columnType !== 'string') {
|
|
31
|
+
return 'TEXT' // Default fallback
|
|
32
|
+
}
|
|
33
|
+
switch (columnType.toLowerCase()) {
|
|
34
|
+
case '_string':
|
|
35
|
+
case '_text':
|
|
36
|
+
case '_mediumtext':
|
|
37
|
+
case '_longtext':
|
|
38
|
+
return 'TEXT'
|
|
39
|
+
case '_number':
|
|
40
|
+
case '_numberkey':
|
|
41
|
+
case '_numbertimestamp':
|
|
42
|
+
case 'integer':
|
|
43
|
+
case 'int':
|
|
44
|
+
return 'INTEGER'
|
|
45
|
+
case '_json':
|
|
46
|
+
return 'TEXT'
|
|
47
|
+
case 'float':
|
|
48
|
+
case 'double':
|
|
49
|
+
case 'real':
|
|
50
|
+
return 'REAL'
|
|
51
|
+
case 'boolean':
|
|
52
|
+
return 'INTEGER'
|
|
53
|
+
case 'date':
|
|
54
|
+
case 'datetime':
|
|
55
|
+
return 'TEXT'
|
|
56
|
+
case 'binary':
|
|
57
|
+
case 'blob':
|
|
58
|
+
return 'BLOB'
|
|
59
|
+
default:
|
|
60
|
+
return 'TEXT'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if we're already in a transaction
|
|
65
|
+
const wasInTransaction = db.inTransaction
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Start a transaction only if we're not already in one
|
|
69
|
+
if (!wasInTransaction) {
|
|
70
|
+
db.prepare('BEGIN').run()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build and execute the CREATE TABLE statement
|
|
74
|
+
let createTableSQL = `CREATE TABLE IF NOT EXISTS \`${inputs.tableName}\` (`
|
|
75
|
+
let columnDefs = inputs.columns.map((column) => {
|
|
76
|
+
const columnType = column.columnType ?? column.type
|
|
77
|
+
let def = `\`${column.columnName}\` ${column.autoIncrement ? 'INTEGER' : getSqliteType(columnType)}`
|
|
78
|
+
if (column.autoIncrement) {
|
|
79
|
+
def += ' PRIMARY KEY AUTOINCREMENT NOT NULL'
|
|
80
|
+
}
|
|
81
|
+
if (column.unique && !column.autoIncrement) def += ' UNIQUE'
|
|
82
|
+
return def
|
|
83
|
+
})
|
|
84
|
+
createTableSQL += columnDefs.join(', ') + ')'
|
|
85
|
+
db.prepare(createTableSQL).run()
|
|
86
|
+
|
|
87
|
+
// Create indexes
|
|
88
|
+
inputs.columns.forEach((column) => {
|
|
89
|
+
if (column.unique && !column.autoIncrement) {
|
|
90
|
+
const indexSQL = `CREATE UNIQUE INDEX IF NOT EXISTS \`idx_${inputs.tableName}_${column.columnName}\` ON \`${inputs.tableName}\` (\`${column.columnName}\`)`
|
|
91
|
+
db.prepare(indexSQL).run()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Commit the transaction only if we started it
|
|
96
|
+
if (!wasInTransaction) {
|
|
97
|
+
db.prepare('COMMIT').run()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return exits.success()
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// If there's an error, roll back the transaction (only if we started it)
|
|
103
|
+
if (!wasInTransaction) {
|
|
104
|
+
db.prepare('ROLLBACK').run()
|
|
105
|
+
}
|
|
106
|
+
return exits.error(
|
|
107
|
+
new Error(`Error defining table ${inputs.tableName}: ${error.message}`)
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Destroy manager',
|
|
3
|
+
|
|
4
|
+
description: 'Destroy the specified SQLite connection manager.',
|
|
5
|
+
|
|
6
|
+
extendedDescription:
|
|
7
|
+
'For SQLite, this involves closing the database connection. Unlike other databases, SQLite does not use connection pools, so this operation is relatively straightforward.',
|
|
8
|
+
|
|
9
|
+
sync: true,
|
|
10
|
+
|
|
11
|
+
inputs: {
|
|
12
|
+
manager: {
|
|
13
|
+
description: 'The SQLite connection manager instance to destroy.',
|
|
14
|
+
extendedDescription:
|
|
15
|
+
'Only managers built using the `createManager()` method of this driver are supported. The database connection manager instance provided must not have been destroyed previously.',
|
|
16
|
+
example: '===',
|
|
17
|
+
required: true
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
meta: {
|
|
21
|
+
friendlyName: 'Meta (custom)',
|
|
22
|
+
description: 'Additional options to pass to the SQLite driver.',
|
|
23
|
+
extendedDescription:
|
|
24
|
+
'This is reserved for custom driver-specific extensions. Please refer to the better-sqlite3 documentation for more specific information.',
|
|
25
|
+
example: '==='
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
exits: {
|
|
30
|
+
success: {
|
|
31
|
+
description: 'The specified SQLite manager was successfully destroyed.',
|
|
32
|
+
outputFriendlyName: 'Report',
|
|
33
|
+
outputDescription:
|
|
34
|
+
'The `meta` property is reserved for custom driver-specific extensions.',
|
|
35
|
+
outputExample: '==='
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
fn: ({ manager, meta }, exits) => {
|
|
40
|
+
try {
|
|
41
|
+
// Validate the manager
|
|
42
|
+
if (
|
|
43
|
+
typeof manager !== 'object' ||
|
|
44
|
+
manager === null ||
|
|
45
|
+
typeof manager.close !== 'function'
|
|
46
|
+
) {
|
|
47
|
+
return exits.error(
|
|
48
|
+
new Error(
|
|
49
|
+
'The provided `manager` is not a valid SQLite manager. It should be a better-sqlite3 Database instance with a `close` method.'
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if the database is already closed
|
|
55
|
+
if (!manager.open) {
|
|
56
|
+
console.warn(
|
|
57
|
+
'SQLite manager appears to already be closed, skipping destruction'
|
|
58
|
+
)
|
|
59
|
+
return exits.success({ meta })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Use graceful cleanup if available (from enhanced create-manager)
|
|
63
|
+
if (typeof manager.closeGracefully === 'function') {
|
|
64
|
+
manager.closeGracefully()
|
|
65
|
+
} else {
|
|
66
|
+
// Fallback to basic close
|
|
67
|
+
manager.close()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Verify the connection is actually closed
|
|
71
|
+
if (manager.open) {
|
|
72
|
+
throw new Error('Failed to close SQLite database connection')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return exits.success({
|
|
76
|
+
meta: {
|
|
77
|
+
...meta,
|
|
78
|
+
destroyedAt: new Date().toISOString()
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return exits.error(
|
|
83
|
+
new Error(`Error destroying SQLite manager: ${error.message}`)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const util = require('util')
|
|
2
|
+
const processNativeRecord = require('./private/process-native-record')
|
|
3
|
+
const buildSqliteWhereClause = require('./private/build-sqlite-where-clause')
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
friendlyName: 'Destroy (records)',
|
|
7
|
+
|
|
8
|
+
description:
|
|
9
|
+
'Destroy record(s) in the SQLite database matching a query criteria.',
|
|
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 physical records that were destroyed.',
|
|
22
|
+
outputExample: '==='
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
fn: async function (inputs, exits) {
|
|
27
|
+
const s3q = inputs.query
|
|
28
|
+
if (s3q.meta && s3q.meta.logSqliteS3Qs) {
|
|
29
|
+
console.log(
|
|
30
|
+
'* * * * * *\nADAPTER (DESTROY RECORDS):',
|
|
31
|
+
util.inspect(s3q, { depth: 5 }),
|
|
32
|
+
'\n'
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tableName = s3q.using
|
|
37
|
+
// Find model by tableName since models is an object, not an array
|
|
38
|
+
let WLModel = null
|
|
39
|
+
for (const modelIdentity in inputs.dryOrm.models) {
|
|
40
|
+
if (inputs.dryOrm.models[modelIdentity].tableName === tableName) {
|
|
41
|
+
WLModel = inputs.dryOrm.models[modelIdentity]
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!WLModel) {
|
|
47
|
+
return exits.error(
|
|
48
|
+
new Error(
|
|
49
|
+
`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.)`
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const pkColumnName = WLModel.attributes[WLModel.primaryKey].columnName
|
|
55
|
+
const isFetchEnabled = !!(s3q.meta && s3q.meta.fetch)
|
|
56
|
+
|
|
57
|
+
const sqliteWhere = buildSqliteWhereClause(
|
|
58
|
+
s3q.criteria.where,
|
|
59
|
+
WLModel,
|
|
60
|
+
s3q.meta
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const db = inputs.connection
|
|
64
|
+
|
|
65
|
+
// Check if we're already in a transaction
|
|
66
|
+
const wasInTransaction = db.inTransaction
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Start a transaction only if we're not already in one
|
|
70
|
+
if (!wasInTransaction) {
|
|
71
|
+
db.exec('BEGIN TRANSACTION')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let phRecords
|
|
75
|
+
if (isFetchEnabled) {
|
|
76
|
+
// Fetch matching records before deletion
|
|
77
|
+
const selectSql = sqliteWhere
|
|
78
|
+
? `SELECT * FROM \`${tableName}\` WHERE ${sqliteWhere}`
|
|
79
|
+
: `SELECT * FROM \`${tableName}\``
|
|
80
|
+
const selectStmt = db.prepare(selectSql)
|
|
81
|
+
phRecords = selectStmt.all()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Perform the deletion
|
|
85
|
+
const deleteSql = sqliteWhere
|
|
86
|
+
? `DELETE FROM \`${tableName}\` WHERE ${sqliteWhere}`
|
|
87
|
+
: `DELETE FROM \`${tableName}\``
|
|
88
|
+
const deleteStmt = db.prepare(deleteSql)
|
|
89
|
+
const deleteInfo = deleteStmt.run()
|
|
90
|
+
|
|
91
|
+
// Commit the transaction only if we started it
|
|
92
|
+
if (!wasInTransaction) {
|
|
93
|
+
db.exec('COMMIT')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!isFetchEnabled) {
|
|
97
|
+
return exits.success()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Process fetched records
|
|
101
|
+
phRecords.forEach((phRecord) => {
|
|
102
|
+
processNativeRecord(phRecord, WLModel, s3q.meta)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return exits.success(phRecords)
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Rollback the transaction in case of error (only if we started it)
|
|
108
|
+
if (!wasInTransaction) {
|
|
109
|
+
db.exec('ROLLBACK')
|
|
110
|
+
}
|
|
111
|
+
return exits.error(err)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
friendlyName: 'Drop (physical model)',
|
|
3
|
+
|
|
4
|
+
description:
|
|
5
|
+
'Completely drop & destroy any traces of a particular physical model (i.e. SQLite table).',
|
|
6
|
+
|
|
7
|
+
sideEffects: 'idempotent',
|
|
8
|
+
|
|
9
|
+
inputs: {
|
|
10
|
+
connection: require('../constants/connection.input'),
|
|
11
|
+
tableName: require('../constants/table-name.input'),
|
|
12
|
+
meta: require('../constants/meta.input')
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
exits: {
|
|
16
|
+
success: {
|
|
17
|
+
description:
|
|
18
|
+
'If such a physical model exists, it was dropped successfully.'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
fn: function (inputs, exits) {
|
|
23
|
+
// Get the SQLite database connection
|
|
24
|
+
const db = inputs.connection
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// SQL to drop the table (properly escape the table name)
|
|
28
|
+
const dropTableSQL = `DROP TABLE IF EXISTS \`${inputs.tableName}\``
|
|
29
|
+
|
|
30
|
+
// Execute the drop table operation
|
|
31
|
+
db.prepare(dropTableSQL).run()
|
|
32
|
+
|
|
33
|
+
// SQL to remove the table's entry from sqlite_sequence (if it exists)
|
|
34
|
+
const cleanSequenceSQL = `DELETE FROM sqlite_sequence WHERE name = ?`
|
|
35
|
+
|
|
36
|
+
// Clean up the sqlite_sequence
|
|
37
|
+
db.prepare(cleanSequenceSQL).run(inputs.tableName)
|
|
38
|
+
|
|
39
|
+
// Return success, as the main operation (dropping the table) was successful
|
|
40
|
+
return exits.success()
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.message.includes('no such table: sqlite_sequence')) {
|
|
43
|
+
// If sqlite_sequence doesn't exist, it's not an error - just means no autoincrement was used
|
|
44
|
+
return exits.success()
|
|
45
|
+
}
|
|
46
|
+
return exits.error(
|
|
47
|
+
new Error(`Error dropping table ${inputs.tableName}: ${error.message}`)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|