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,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
+ }