@dockstat/sqlite-wrapper 1.2.7 → 1.3.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/README.md +493 -39
- package/index.ts +436 -156
- package/package.json +11 -5
- package/query-builder/base.ts +103 -141
- package/query-builder/delete.ts +276 -187
- package/query-builder/index.ts +95 -117
- package/query-builder/insert.ts +184 -153
- package/query-builder/select.ts +155 -180
- package/query-builder/update.ts +195 -165
- package/query-builder/where.ts +165 -200
- package/types.ts +134 -149
- package/utils/index.ts +44 -0
- package/utils/logger.ts +184 -0
- package/utils/sql.ts +241 -0
- package/utils/transformer.ts +256 -0
package/index.ts
CHANGED
|
@@ -1,52 +1,49 @@
|
|
|
1
|
-
import { Database, type SQLQueryBindings } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
ColumnConstraints,
|
|
6
|
-
ColumnDefinition,
|
|
7
|
-
DefaultExpression,
|
|
8
|
-
ForeignKeyAction,
|
|
9
|
-
JsonColumnConfig,
|
|
10
|
-
SQLiteType,
|
|
11
|
-
TableConstraints,
|
|
12
|
-
TableOptions,
|
|
13
|
-
TableSchema,
|
|
14
|
-
} from './types'
|
|
1
|
+
import { Database, type SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
import { QueryBuilder } from "./query-builder/index"
|
|
3
|
+
import type { ColumnDefinition, Parser, TableConstraints, TableOptions, TableSchema } from "./types"
|
|
4
|
+
import { addLoggerParents as addParents, createLogger, logger as sqliteLogger } from "./utils"
|
|
15
5
|
|
|
16
|
-
|
|
6
|
+
// Re-export logger utilities for external use
|
|
7
|
+
export const logger = sqliteLogger
|
|
8
|
+
export const addLoggerParents = addParents
|
|
9
|
+
|
|
10
|
+
// Internal loggers for different components
|
|
11
|
+
const dbLog = createLogger("db")
|
|
12
|
+
const backupLog = createLogger("backup")
|
|
13
|
+
const tableLog = createLogger("table")
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
16
|
* Re-export all types and utilities
|
|
20
17
|
*/
|
|
21
18
|
export { QueryBuilder }
|
|
22
19
|
export type {
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
ArrayKey,
|
|
21
|
+
ColumnConstraints,
|
|
22
|
+
ColumnDefinition,
|
|
23
|
+
ColumnNames,
|
|
24
|
+
DefaultExpression,
|
|
25
25
|
DeleteResult,
|
|
26
|
+
ForeignKeyAction,
|
|
26
27
|
InsertOptions,
|
|
27
|
-
|
|
28
|
-
WhereCondition,
|
|
28
|
+
InsertResult,
|
|
29
29
|
RegexCondition,
|
|
30
|
-
JsonColumnConfig,
|
|
31
|
-
TableSchema,
|
|
32
|
-
ColumnDefinition,
|
|
33
30
|
SQLiteType,
|
|
34
|
-
ColumnConstraints,
|
|
35
|
-
TableOptions,
|
|
36
|
-
DefaultExpression,
|
|
37
|
-
ForeignKeyAction,
|
|
38
31
|
TableConstraints,
|
|
39
|
-
|
|
32
|
+
TableOptions,
|
|
33
|
+
TableSchema,
|
|
34
|
+
UpdateResult,
|
|
35
|
+
WhereCondition,
|
|
36
|
+
} from "./types"
|
|
40
37
|
|
|
41
38
|
// Re-export helper utilities
|
|
42
39
|
export {
|
|
43
40
|
column,
|
|
44
|
-
|
|
45
|
-
SQLiteTypes,
|
|
41
|
+
defaultExpr,
|
|
46
42
|
SQLiteFunctions,
|
|
47
43
|
SQLiteKeywords,
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
SQLiteTypes,
|
|
45
|
+
sql,
|
|
46
|
+
} from "./types"
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
49
|
* TypedSQLite — comprehensive wrapper around bun:sqlite `Database`.
|
|
@@ -60,8 +57,41 @@ export {
|
|
|
60
57
|
* - JSON column support
|
|
61
58
|
* - And much more...
|
|
62
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* Auto-backup configuration options
|
|
62
|
+
*/
|
|
63
|
+
export interface AutoBackupOptions {
|
|
64
|
+
/** Enable automatic backups */
|
|
65
|
+
enabled: boolean
|
|
66
|
+
/** Directory to store backup files */
|
|
67
|
+
directory: string
|
|
68
|
+
/** Backup interval in milliseconds (default: 1 hour) */
|
|
69
|
+
intervalMs?: number
|
|
70
|
+
/** Maximum number of backups to retain (default: 10) */
|
|
71
|
+
maxBackups?: number
|
|
72
|
+
/** Prefix for backup filenames (default: 'backup') */
|
|
73
|
+
filenamePrefix?: string
|
|
74
|
+
/** Whether to compress backups using gzip (default: false) */
|
|
75
|
+
compress?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Database configuration options
|
|
80
|
+
*/
|
|
81
|
+
export interface DBOptions {
|
|
82
|
+
/** PRAGMA settings to apply on database open */
|
|
83
|
+
pragmas?: Array<[string, SQLQueryBindings]>
|
|
84
|
+
/** Paths to SQLite extensions to load */
|
|
85
|
+
loadExtensions?: string[]
|
|
86
|
+
/** Auto-backup configuration */
|
|
87
|
+
autoBackup?: AutoBackupOptions
|
|
88
|
+
}
|
|
89
|
+
|
|
63
90
|
class DB {
|
|
64
|
-
|
|
91
|
+
protected db: Database
|
|
92
|
+
protected dbPath: string
|
|
93
|
+
private autoBackupTimer: ReturnType<typeof setInterval> | null = null
|
|
94
|
+
private autoBackupOptions: AutoBackupOptions | null = null
|
|
65
95
|
|
|
66
96
|
/**
|
|
67
97
|
* Open or create a SQLite database at `path`.
|
|
@@ -69,14 +99,9 @@ class DB {
|
|
|
69
99
|
* @param path - Path to the SQLite file (e.g. "app.db"). Use ":memory:" for in-memory DB.
|
|
70
100
|
* @param options - Optional database configuration
|
|
71
101
|
*/
|
|
72
|
-
constructor(
|
|
73
|
-
path
|
|
74
|
-
|
|
75
|
-
pragmas?: Array<[string, SQLQueryBindings]>
|
|
76
|
-
loadExtensions?: string[]
|
|
77
|
-
}
|
|
78
|
-
) {
|
|
79
|
-
logger.info(`Opening database: ${path}`)
|
|
102
|
+
constructor(path: string, options?: DBOptions) {
|
|
103
|
+
dbLog.connection(path, "open")
|
|
104
|
+
this.dbPath = path
|
|
80
105
|
this.db = new Database(path)
|
|
81
106
|
|
|
82
107
|
// Apply PRAGMA settings if provided
|
|
@@ -92,6 +117,231 @@ class DB {
|
|
|
92
117
|
this.loadExtension(extensionPath)
|
|
93
118
|
}
|
|
94
119
|
}
|
|
120
|
+
|
|
121
|
+
// Setup auto-backup if configured
|
|
122
|
+
if (options?.autoBackup?.enabled) {
|
|
123
|
+
this.setupAutoBackup(options.autoBackup)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Setup automatic backup with retention policy
|
|
129
|
+
*/
|
|
130
|
+
private setupAutoBackup(options: AutoBackupOptions): void {
|
|
131
|
+
if (this.dbPath === ":memory:") {
|
|
132
|
+
backupLog.warn("Auto-backup is not available for in-memory databases")
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.autoBackupOptions = {
|
|
137
|
+
enabled: options.enabled,
|
|
138
|
+
directory: options.directory,
|
|
139
|
+
intervalMs: options.intervalMs ?? 60 * 60 * 1000, // Default: 1 hour
|
|
140
|
+
maxBackups: options.maxBackups ?? 10,
|
|
141
|
+
filenamePrefix: options.filenamePrefix ?? "backup",
|
|
142
|
+
compress: options.compress ?? false,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Ensure backup directory exists
|
|
146
|
+
const fs = require("node:fs")
|
|
147
|
+
if (!fs.existsSync(this.autoBackupOptions.directory)) {
|
|
148
|
+
fs.mkdirSync(this.autoBackupOptions.directory, { recursive: true })
|
|
149
|
+
backupLog.info(`Created backup directory: ${this.autoBackupOptions.directory}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Create initial backup
|
|
153
|
+
this.backup()
|
|
154
|
+
|
|
155
|
+
// Setup interval for periodic backups
|
|
156
|
+
this.autoBackupTimer = setInterval(() => {
|
|
157
|
+
this.backup()
|
|
158
|
+
}, this.autoBackupOptions.intervalMs)
|
|
159
|
+
|
|
160
|
+
backupLog.info(
|
|
161
|
+
`Auto-backup enabled: interval=${this.autoBackupOptions.intervalMs}ms, maxBackups=${this.autoBackupOptions.maxBackups}`
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a backup of the database
|
|
167
|
+
*
|
|
168
|
+
* @param customPath - Optional custom path for the backup file. If not provided, uses auto-backup settings or generates a timestamped filename.
|
|
169
|
+
* @returns The path to the created backup file
|
|
170
|
+
*/
|
|
171
|
+
backup(customPath?: string): string {
|
|
172
|
+
if (this.dbPath === ":memory:") {
|
|
173
|
+
throw new Error("Cannot backup an in-memory database")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const path = require("node:path")
|
|
177
|
+
|
|
178
|
+
let backupPath: string
|
|
179
|
+
|
|
180
|
+
if (customPath) {
|
|
181
|
+
backupPath = customPath
|
|
182
|
+
} else if (this.autoBackupOptions) {
|
|
183
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
184
|
+
const filename = `${this.autoBackupOptions.filenamePrefix}_${timestamp}.db`
|
|
185
|
+
backupPath = path.join(this.autoBackupOptions.directory, filename)
|
|
186
|
+
} else {
|
|
187
|
+
// Generate a default backup path next to the database file
|
|
188
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
189
|
+
const dir = path.dirname(this.dbPath)
|
|
190
|
+
const basename = path.basename(this.dbPath, path.extname(this.dbPath))
|
|
191
|
+
backupPath = path.join(dir, `${basename}_backup_${timestamp}.db`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Use SQLite's backup API via VACUUM INTO for a consistent backup
|
|
195
|
+
try {
|
|
196
|
+
this.db.run(`VACUUM INTO '${backupPath.replace(/'/g, "''")}'`)
|
|
197
|
+
backupLog.backup("create", backupPath)
|
|
198
|
+
|
|
199
|
+
// Apply retention policy if auto-backup is enabled
|
|
200
|
+
if (this.autoBackupOptions) {
|
|
201
|
+
this.applyRetentionPolicy()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return backupPath
|
|
205
|
+
} catch (error) {
|
|
206
|
+
backupLog.error(`Failed to create backup: ${error}`)
|
|
207
|
+
throw error
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Apply retention policy to remove old backups
|
|
213
|
+
*/
|
|
214
|
+
private applyRetentionPolicy(): void {
|
|
215
|
+
if (!this.autoBackupOptions) return
|
|
216
|
+
|
|
217
|
+
const fs = require("node:fs")
|
|
218
|
+
const path = require("node:path")
|
|
219
|
+
|
|
220
|
+
const backupDir = this.autoBackupOptions.directory
|
|
221
|
+
const prefix = this.autoBackupOptions.filenamePrefix || "backup"
|
|
222
|
+
const maxBackups = this.autoBackupOptions.maxBackups || 10
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
// Get all backup files matching the pattern
|
|
226
|
+
const files = fs
|
|
227
|
+
.readdirSync(backupDir)
|
|
228
|
+
.filter((file: string) => file.startsWith(prefix) && file.endsWith(".db"))
|
|
229
|
+
.map((file: string) => ({
|
|
230
|
+
name: file,
|
|
231
|
+
path: path.join(backupDir, file),
|
|
232
|
+
mtime: fs.statSync(path.join(backupDir, file)).mtime.getTime(),
|
|
233
|
+
}))
|
|
234
|
+
.sort((a: { mtime: number }, b: { mtime: number }) => b.mtime - a.mtime) // Sort by newest first
|
|
235
|
+
|
|
236
|
+
// Remove excess backups
|
|
237
|
+
if (files.length > maxBackups) {
|
|
238
|
+
const toDelete = files.slice(maxBackups)
|
|
239
|
+
for (const file of toDelete) {
|
|
240
|
+
fs.unlinkSync(file.path)
|
|
241
|
+
backupLog.debug(`Removed old backup: ${file.name}`)
|
|
242
|
+
}
|
|
243
|
+
backupLog.info(`Retention policy applied: removed ${toDelete.length} old backup(s)`)
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
backupLog.error(`Failed to apply retention policy: ${error}`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* List all available backups
|
|
252
|
+
*
|
|
253
|
+
* @returns Array of backup file information
|
|
254
|
+
*/
|
|
255
|
+
listBackups(): Array<{ filename: string; path: string; size: number; created: Date }> {
|
|
256
|
+
if (!this.autoBackupOptions) {
|
|
257
|
+
backupLog.warn("Auto-backup is not configured. Use backup() with a custom path instead.")
|
|
258
|
+
return []
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const fs = require("node:fs")
|
|
262
|
+
const path = require("node:path")
|
|
263
|
+
|
|
264
|
+
const backupDir = this.autoBackupOptions.directory
|
|
265
|
+
const prefix = this.autoBackupOptions.filenamePrefix || "backup"
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
return fs
|
|
269
|
+
.readdirSync(backupDir)
|
|
270
|
+
.filter((file: string) => file.startsWith(prefix) && file.endsWith(".db"))
|
|
271
|
+
.map((file: string) => {
|
|
272
|
+
const filePath = path.join(backupDir, file)
|
|
273
|
+
const stats = fs.statSync(filePath)
|
|
274
|
+
return {
|
|
275
|
+
filename: file,
|
|
276
|
+
path: filePath,
|
|
277
|
+
size: stats.size,
|
|
278
|
+
created: stats.mtime,
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
.sort(
|
|
282
|
+
(a: { created: Date }, b: { created: Date }) => b.created.getTime() - a.created.getTime()
|
|
283
|
+
)
|
|
284
|
+
} catch (error) {
|
|
285
|
+
backupLog.error(`Failed to list backups: ${error}`)
|
|
286
|
+
return []
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Restore database from a backup file
|
|
292
|
+
*
|
|
293
|
+
* @param backupPath - Path to the backup file to restore from
|
|
294
|
+
* @param targetPath - Optional target path. If not provided, restores to the original database path.
|
|
295
|
+
*/
|
|
296
|
+
restore(backupPath: string, targetPath?: string): void {
|
|
297
|
+
const fs = require("node:fs")
|
|
298
|
+
|
|
299
|
+
if (!fs.existsSync(backupPath)) {
|
|
300
|
+
throw new Error(`Backup file not found: ${backupPath}`)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const restorePath = targetPath || this.dbPath
|
|
304
|
+
|
|
305
|
+
if (restorePath === ":memory:") {
|
|
306
|
+
throw new Error("Cannot restore to an in-memory database path")
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Close current connection if restoring to the same path
|
|
310
|
+
if (restorePath === this.dbPath) {
|
|
311
|
+
this.db.close()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
fs.copyFileSync(backupPath, restorePath)
|
|
316
|
+
backupLog.backup("restore", backupPath)
|
|
317
|
+
|
|
318
|
+
// Reopen database if we closed it
|
|
319
|
+
if (restorePath === this.dbPath) {
|
|
320
|
+
this.db = new Database(this.dbPath)
|
|
321
|
+
dbLog.info("Database connection reopened after restore")
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
backupLog.error(`Failed to restore backup: ${error}`)
|
|
325
|
+
throw error
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Stop auto-backup if it's running
|
|
331
|
+
*/
|
|
332
|
+
stopAutoBackup(): void {
|
|
333
|
+
if (this.autoBackupTimer) {
|
|
334
|
+
clearInterval(this.autoBackupTimer)
|
|
335
|
+
this.autoBackupTimer = null
|
|
336
|
+
backupLog.info("Auto-backup stopped")
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get the database file path
|
|
342
|
+
*/
|
|
343
|
+
getPath(): string {
|
|
344
|
+
return this.dbPath
|
|
95
345
|
}
|
|
96
346
|
|
|
97
347
|
/**
|
|
@@ -100,17 +350,25 @@ class DB {
|
|
|
100
350
|
*/
|
|
101
351
|
table<T extends Record<string, unknown>>(
|
|
102
352
|
tableName: string,
|
|
103
|
-
|
|
353
|
+
parser: Partial<Parser<T>>
|
|
104
354
|
): QueryBuilder<T> {
|
|
105
|
-
|
|
106
|
-
|
|
355
|
+
const pObj: Parser<T> = {
|
|
356
|
+
JSON: parser.JSON || [],
|
|
357
|
+
MODULE: parser.MODULE || {},
|
|
358
|
+
BOOLEAN: parser.BOOLEAN || [],
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
tableLog.debug(`Creating QueryBuilder for: ${tableName}`)
|
|
362
|
+
return new QueryBuilder<T>(this.db, tableName, pObj)
|
|
107
363
|
}
|
|
108
364
|
|
|
109
365
|
/**
|
|
110
366
|
* Close the underlying SQLite database handle.
|
|
367
|
+
* Also stops auto-backup if it's running.
|
|
111
368
|
*/
|
|
112
369
|
close(): void {
|
|
113
|
-
|
|
370
|
+
dbLog.connection(this.dbPath, "close")
|
|
371
|
+
this.stopAutoBackup()
|
|
114
372
|
this.db.close()
|
|
115
373
|
}
|
|
116
374
|
|
|
@@ -207,28 +465,22 @@ class DB {
|
|
|
207
465
|
*
|
|
208
466
|
* @throws {Error} If column definitions are invalid or constraints conflict.
|
|
209
467
|
*/
|
|
210
|
-
createTable<_T extends Record<string, unknown>>(
|
|
468
|
+
createTable<_T extends Record<string, unknown> = Record<string, unknown>>(
|
|
211
469
|
tableName: string,
|
|
212
|
-
columns:
|
|
213
|
-
options?: TableOptions<_T
|
|
470
|
+
columns: Record<keyof _T, ColumnDefinition>,
|
|
471
|
+
options?: TableOptions<_T>
|
|
214
472
|
): QueryBuilder<_T> {
|
|
215
|
-
const temp = options?.temporary ?
|
|
216
|
-
const ifNot = options?.ifNotExists ?
|
|
217
|
-
const withoutRowId = options?.withoutRowId ?
|
|
473
|
+
const temp = options?.temporary ? "TEMPORARY " : tableName === ":memory" ? "TEMPORARY " : ""
|
|
474
|
+
const ifNot = options?.ifNotExists ? "IF NOT EXISTS " : ""
|
|
475
|
+
const withoutRowId = options?.withoutRowId ? " WITHOUT ROWID" : ""
|
|
218
476
|
|
|
219
477
|
const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
|
|
220
478
|
|
|
221
479
|
let columnDefs: string
|
|
222
480
|
let tableConstraints: string[] = []
|
|
223
481
|
|
|
224
|
-
if (
|
|
225
|
-
//
|
|
226
|
-
columnDefs = columns.trim()
|
|
227
|
-
if (!columnDefs) {
|
|
228
|
-
throw new Error('Empty column definition string')
|
|
229
|
-
}
|
|
230
|
-
} else if (this.isTableSchema(columns)) {
|
|
231
|
-
// New comprehensive type-safe approach
|
|
482
|
+
if (this.isTableSchema(columns)) {
|
|
483
|
+
// comprehensive type-safe approach
|
|
232
484
|
const parts: string[] = []
|
|
233
485
|
for (const [colName, colDef] of Object.entries(columns)) {
|
|
234
486
|
if (!colName) continue
|
|
@@ -238,9 +490,10 @@ class DB {
|
|
|
238
490
|
}
|
|
239
491
|
|
|
240
492
|
if (parts.length === 0) {
|
|
241
|
-
throw new Error(
|
|
493
|
+
throw new Error("No columns provided")
|
|
242
494
|
}
|
|
243
|
-
|
|
495
|
+
|
|
496
|
+
columnDefs = parts.join(", ")
|
|
244
497
|
|
|
245
498
|
// Add table-level constraints
|
|
246
499
|
if (options?.constraints) {
|
|
@@ -249,10 +502,10 @@ class DB {
|
|
|
249
502
|
} else {
|
|
250
503
|
// Original object-based approach
|
|
251
504
|
const parts: string[] = []
|
|
252
|
-
for (const [col, def] of Object.entries(columns)) {
|
|
505
|
+
for (const [col, def] of Object.entries(columns as Record<string, string>)) {
|
|
253
506
|
if (!col) continue
|
|
254
507
|
|
|
255
|
-
const defTrim = (def
|
|
508
|
+
const defTrim = (def || "").trim()
|
|
256
509
|
if (!defTrim) {
|
|
257
510
|
throw new Error(`Missing SQL type/constraints for column "${col}"`)
|
|
258
511
|
}
|
|
@@ -260,15 +513,19 @@ class DB {
|
|
|
260
513
|
}
|
|
261
514
|
|
|
262
515
|
if (parts.length === 0) {
|
|
263
|
-
throw new Error(
|
|
516
|
+
throw new Error("No columns provided")
|
|
264
517
|
}
|
|
265
|
-
columnDefs = parts.join(
|
|
518
|
+
columnDefs = parts.join(", ")
|
|
266
519
|
}
|
|
267
520
|
|
|
268
|
-
|
|
269
|
-
const allDefinitions = [columnDefs, ...tableConstraints].join(', ')
|
|
521
|
+
const allDefinitions = [columnDefs, ...tableConstraints].join(", ")
|
|
270
522
|
|
|
271
|
-
const
|
|
523
|
+
const columnNames = Object.keys(columns)
|
|
524
|
+
tableLog.tableCreate(tableName, columnNames)
|
|
525
|
+
|
|
526
|
+
const sql = `CREATE ${temp}TABLE ${ifNot}${quoteIdent(
|
|
527
|
+
tableName
|
|
528
|
+
)} (${allDefinitions})${withoutRowId};`
|
|
272
529
|
|
|
273
530
|
this.db.run(sql)
|
|
274
531
|
|
|
@@ -277,7 +534,39 @@ class DB {
|
|
|
277
534
|
this.setTableComment(tableName, options.comment)
|
|
278
535
|
}
|
|
279
536
|
|
|
280
|
-
|
|
537
|
+
// Auto-detect JSON and BOOLEAN columns from schema
|
|
538
|
+
const autoDetectedJson: Array<keyof _T> = []
|
|
539
|
+
const autoDetectedBoolean: Array<keyof _T> = []
|
|
540
|
+
|
|
541
|
+
if (this.isTableSchema(columns)) {
|
|
542
|
+
for (const [colName, colDef] of Object.entries(columns)) {
|
|
543
|
+
if (colDef.type === "JSON") {
|
|
544
|
+
autoDetectedJson.push(colName as keyof _T)
|
|
545
|
+
}
|
|
546
|
+
if (colDef.type === "BOOLEAN") {
|
|
547
|
+
autoDetectedBoolean.push(colName as keyof _T)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Merge auto-detected columns with user-provided parser options
|
|
553
|
+
const userJson = options?.parser?.JSON || []
|
|
554
|
+
const userBoolean = options?.parser?.BOOLEAN || []
|
|
555
|
+
const userModule = options?.parser?.MODULE || {}
|
|
556
|
+
|
|
557
|
+
// Combine and deduplicate
|
|
558
|
+
const mergedJson = [...new Set([...autoDetectedJson, ...userJson])] as Array<keyof _T>
|
|
559
|
+
const mergedBoolean = [...new Set([...autoDetectedBoolean, ...userBoolean])] as Array<keyof _T>
|
|
560
|
+
|
|
561
|
+
const pObj = {
|
|
562
|
+
JSON: mergedJson,
|
|
563
|
+
MODULE: userModule,
|
|
564
|
+
BOOLEAN: mergedBoolean,
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
tableLog.parserConfig(pObj.JSON.map(String), pObj.BOOLEAN.map(String), Object.keys(pObj.MODULE))
|
|
568
|
+
|
|
569
|
+
return this.table<_T>(tableName, pObj)
|
|
281
570
|
}
|
|
282
571
|
|
|
283
572
|
/**
|
|
@@ -294,15 +583,17 @@ class DB {
|
|
|
294
583
|
partial?: string
|
|
295
584
|
}
|
|
296
585
|
): void {
|
|
297
|
-
const unique = options?.unique ?
|
|
298
|
-
const ifNot = options?.ifNotExists ?
|
|
586
|
+
const unique = options?.unique ? "UNIQUE " : ""
|
|
587
|
+
const ifNot = options?.ifNotExists ? "IF NOT EXISTS " : ""
|
|
299
588
|
const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
|
|
300
589
|
|
|
301
590
|
const columnList = Array.isArray(columns)
|
|
302
|
-
? columns.map(quoteIdent).join(
|
|
591
|
+
? columns.map(quoteIdent).join(", ")
|
|
303
592
|
: quoteIdent(columns)
|
|
304
593
|
|
|
305
|
-
let sql = `CREATE ${unique}INDEX ${ifNot}${quoteIdent(
|
|
594
|
+
let sql = `CREATE ${unique}INDEX ${ifNot}${quoteIdent(
|
|
595
|
+
indexName
|
|
596
|
+
)} ON ${quoteIdent(tableName)} (${columnList})`
|
|
306
597
|
|
|
307
598
|
if (options?.where) {
|
|
308
599
|
sql += ` WHERE ${options.where}`
|
|
@@ -315,7 +606,7 @@ class DB {
|
|
|
315
606
|
* Drop a table
|
|
316
607
|
*/
|
|
317
608
|
dropTable(tableName: string, options?: { ifExists?: boolean }): void {
|
|
318
|
-
const ifExists = options?.ifExists ?
|
|
609
|
+
const ifExists = options?.ifExists ? "IF EXISTS " : ""
|
|
319
610
|
const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
|
|
320
611
|
|
|
321
612
|
const sql = `DROP TABLE ${ifExists}${quoteIdent(tableName)};`
|
|
@@ -326,7 +617,7 @@ class DB {
|
|
|
326
617
|
* Drop an index
|
|
327
618
|
*/
|
|
328
619
|
dropIndex(indexName: string, options?: { ifExists?: boolean }): void {
|
|
329
|
-
const ifExists = options?.ifExists ?
|
|
620
|
+
const ifExists = options?.ifExists ? "IF EXISTS " : ""
|
|
330
621
|
const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
|
|
331
622
|
|
|
332
623
|
const sql = `DROP INDEX ${ifExists}${quoteIdent(indexName)};`
|
|
@@ -337,40 +628,40 @@ class DB {
|
|
|
337
628
|
* Type guard to check if columns definition is a TableSchema
|
|
338
629
|
*/
|
|
339
630
|
private isTableSchema(columns: unknown): columns is TableSchema {
|
|
340
|
-
if (typeof columns !==
|
|
631
|
+
if (typeof columns !== "object" || columns === null) {
|
|
341
632
|
return false
|
|
342
633
|
}
|
|
343
634
|
|
|
344
635
|
// Check if any value has a 'type' property with a valid SQLite type
|
|
345
636
|
for (const [_key, value] of Object.entries(columns)) {
|
|
346
|
-
if (typeof value ===
|
|
637
|
+
if (typeof value === "object" && value !== null && "type" in value) {
|
|
347
638
|
const type = (value as { type: string }).type
|
|
348
639
|
const validTypes = [
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
640
|
+
"INTEGER",
|
|
641
|
+
"TEXT",
|
|
642
|
+
"REAL",
|
|
643
|
+
"BLOB",
|
|
644
|
+
"NUMERIC",
|
|
645
|
+
"INT",
|
|
646
|
+
"TINYINT",
|
|
647
|
+
"SMALLINT",
|
|
648
|
+
"MEDIUMINT",
|
|
649
|
+
"BIGINT",
|
|
650
|
+
"VARCHAR",
|
|
651
|
+
"CHAR",
|
|
652
|
+
"CHARACTER",
|
|
653
|
+
"NCHAR",
|
|
654
|
+
"NVARCHAR",
|
|
655
|
+
"CLOB",
|
|
656
|
+
"DOUBLE",
|
|
657
|
+
"FLOAT",
|
|
658
|
+
"DECIMAL",
|
|
659
|
+
"DATE",
|
|
660
|
+
"DATETIME",
|
|
661
|
+
"TIMESTAMP",
|
|
662
|
+
"TIME",
|
|
663
|
+
"BOOLEAN",
|
|
664
|
+
"JSON",
|
|
374
665
|
]
|
|
375
666
|
if (validTypes.includes(type)) {
|
|
376
667
|
return true
|
|
@@ -402,40 +693,37 @@ class DB {
|
|
|
402
693
|
|
|
403
694
|
// Add PRIMARY KEY (must come before AUTOINCREMENT)
|
|
404
695
|
if (colDef.primaryKey) {
|
|
405
|
-
parts.push(
|
|
696
|
+
parts.push("PRIMARY KEY")
|
|
406
697
|
}
|
|
407
698
|
|
|
408
699
|
// Add AUTOINCREMENT (only valid with INTEGER PRIMARY KEY)
|
|
409
700
|
if (colDef.autoincrement) {
|
|
410
|
-
if (!colDef.type.includes(
|
|
701
|
+
if (!colDef.type.includes("INT") || !colDef.primaryKey) {
|
|
411
702
|
throw new Error(
|
|
412
703
|
`AUTOINCREMENT can only be used with INTEGER PRIMARY KEY columns (column: ${columnName})`
|
|
413
704
|
)
|
|
414
705
|
}
|
|
415
|
-
parts.push(
|
|
706
|
+
parts.push("AUTOINCREMENT")
|
|
416
707
|
}
|
|
417
708
|
|
|
418
709
|
// Add NOT NULL (but skip if PRIMARY KEY is already specified, as it's implicit)
|
|
419
710
|
if (colDef.notNull && !colDef.primaryKey) {
|
|
420
|
-
parts.push(
|
|
711
|
+
parts.push("NOT NULL")
|
|
421
712
|
}
|
|
422
713
|
|
|
423
714
|
// Add UNIQUE
|
|
424
715
|
if (colDef.unique) {
|
|
425
|
-
parts.push(
|
|
716
|
+
parts.push("UNIQUE")
|
|
426
717
|
}
|
|
427
718
|
|
|
428
719
|
// Add DEFAULT
|
|
429
720
|
if (colDef.default !== undefined) {
|
|
430
721
|
if (colDef.default === null) {
|
|
431
|
-
parts.push(
|
|
432
|
-
} else if (
|
|
433
|
-
typeof colDef.default === 'object' &&
|
|
434
|
-
colDef.default._type === 'expression'
|
|
435
|
-
) {
|
|
722
|
+
parts.push("DEFAULT NULL")
|
|
723
|
+
} else if (typeof colDef.default === "object" && colDef.default._type === "expression") {
|
|
436
724
|
// Handle DefaultExpression
|
|
437
725
|
parts.push(`DEFAULT (${colDef.default.expression})`)
|
|
438
|
-
} else if (typeof colDef.default ===
|
|
726
|
+
} else if (typeof colDef.default === "string") {
|
|
439
727
|
// Handle string defaults - check if it's a function call or literal
|
|
440
728
|
if (this.isSQLFunction(colDef.default)) {
|
|
441
729
|
parts.push(`DEFAULT (${colDef.default})`)
|
|
@@ -443,7 +731,7 @@ class DB {
|
|
|
443
731
|
// Literal string value
|
|
444
732
|
parts.push(`DEFAULT '${colDef.default.replace(/'/g, "''")}'`)
|
|
445
733
|
}
|
|
446
|
-
} else if (typeof colDef.default ===
|
|
734
|
+
} else if (typeof colDef.default === "boolean") {
|
|
447
735
|
parts.push(`DEFAULT ${colDef.default ? 1 : 0}`)
|
|
448
736
|
} else {
|
|
449
737
|
parts.push(`DEFAULT ${colDef.default}`)
|
|
@@ -467,7 +755,10 @@ class DB {
|
|
|
467
755
|
// Add REFERENCES (foreign key)
|
|
468
756
|
if (colDef.references) {
|
|
469
757
|
const ref = colDef.references
|
|
470
|
-
let refClause = `REFERENCES "${ref.table.replace(
|
|
758
|
+
let refClause = `REFERENCES "${ref.table.replace(
|
|
759
|
+
/"/g,
|
|
760
|
+
'""'
|
|
761
|
+
)}"("${ref.column.replace(/"/g, '""')}")`
|
|
471
762
|
|
|
472
763
|
if (ref.onDelete) {
|
|
473
764
|
refClause += ` ON DELETE ${ref.onDelete}`
|
|
@@ -482,26 +773,23 @@ class DB {
|
|
|
482
773
|
|
|
483
774
|
// Add GENERATED column
|
|
484
775
|
if (colDef.generated) {
|
|
485
|
-
const storageType = colDef.generated.stored ?
|
|
486
|
-
parts.push(
|
|
487
|
-
`GENERATED ALWAYS AS (${colDef.generated.expression}) ${storageType}`
|
|
488
|
-
)
|
|
776
|
+
const storageType = colDef.generated.stored ? "STORED" : "VIRTUAL"
|
|
777
|
+
parts.push(`GENERATED ALWAYS AS (${colDef.generated.expression}) ${storageType}`)
|
|
489
778
|
}
|
|
490
|
-
|
|
491
|
-
return parts.join(' ')
|
|
779
|
+
return parts.join(" ")
|
|
492
780
|
}
|
|
493
781
|
|
|
494
782
|
/**
|
|
495
783
|
* Build table-level constraints
|
|
496
784
|
*/
|
|
497
|
-
private buildTableConstraints(constraints: TableConstraints): string[] {
|
|
785
|
+
private buildTableConstraints<T>(constraints: TableConstraints<T>): string[] {
|
|
498
786
|
const parts: string[] = []
|
|
499
787
|
|
|
500
788
|
// PRIMARY KEY constraint
|
|
501
789
|
if (constraints.primaryKey && constraints.primaryKey.length > 0) {
|
|
502
790
|
const columns = constraints.primaryKey
|
|
503
|
-
.map((col) => `"${col.replace(/"/g, '""')}"`)
|
|
504
|
-
.join(
|
|
791
|
+
.map((col) => `"${String(col).replace(/"/g, '""')}"`)
|
|
792
|
+
.join(", ")
|
|
505
793
|
parts.push(`PRIMARY KEY (${columns})`)
|
|
506
794
|
}
|
|
507
795
|
|
|
@@ -510,16 +798,14 @@ class DB {
|
|
|
510
798
|
if (Array.isArray(constraints.unique[0])) {
|
|
511
799
|
// Multiple composite unique constraints
|
|
512
800
|
for (const uniqueGroup of constraints.unique as string[][]) {
|
|
513
|
-
const columns = uniqueGroup
|
|
514
|
-
.map((col) => `"${col.replace(/"/g, '""')}"`)
|
|
515
|
-
.join(', ')
|
|
801
|
+
const columns = uniqueGroup.map((col) => `"${col.replace(/"/g, '""')}"`).join(", ")
|
|
516
802
|
parts.push(`UNIQUE (${columns})`)
|
|
517
803
|
}
|
|
518
804
|
} else {
|
|
519
805
|
// Single unique constraint
|
|
520
806
|
const columns = (constraints.unique as string[])
|
|
521
807
|
.map((col) => `"${col.replace(/"/g, '""')}"`)
|
|
522
|
-
.join(
|
|
808
|
+
.join(", ")
|
|
523
809
|
parts.push(`UNIQUE (${columns})`)
|
|
524
810
|
}
|
|
525
811
|
}
|
|
@@ -534,14 +820,15 @@ class DB {
|
|
|
534
820
|
// FOREIGN KEY constraints
|
|
535
821
|
if (constraints.foreignKeys) {
|
|
536
822
|
for (const fk of constraints.foreignKeys) {
|
|
537
|
-
const columns = fk.columns
|
|
538
|
-
.map((col) => `"${col.replace(/"/g, '""')}"`)
|
|
539
|
-
.join(', ')
|
|
823
|
+
const columns = fk.columns.map((col) => `"${String(col).replace(/"/g, '""')}"`).join(", ")
|
|
540
824
|
const refColumns = fk.references.columns
|
|
541
825
|
.map((col) => `"${col.replace(/"/g, '""')}"`)
|
|
542
|
-
.join(
|
|
826
|
+
.join(", ")
|
|
543
827
|
|
|
544
|
-
let fkClause = `FOREIGN KEY (${columns}) REFERENCES "${fk.references.table.replace(
|
|
828
|
+
let fkClause = `FOREIGN KEY (${columns}) REFERENCES "${fk.references.table.replace(
|
|
829
|
+
/"/g,
|
|
830
|
+
'""'
|
|
831
|
+
)}" (${refColumns})`
|
|
545
832
|
|
|
546
833
|
if (fk.references.onDelete) {
|
|
547
834
|
fkClause += ` ON DELETE ${fk.references.onDelete}`
|
|
@@ -596,7 +883,7 @@ class DB {
|
|
|
596
883
|
stmt.run(tableName, comment)
|
|
597
884
|
} catch (error) {
|
|
598
885
|
// Silently ignore if we can't create metadata table
|
|
599
|
-
|
|
886
|
+
logger.warn(`Could not store table comment for ${tableName}: ${error}`)
|
|
600
887
|
}
|
|
601
888
|
}
|
|
602
889
|
|
|
@@ -640,8 +927,8 @@ class DB {
|
|
|
640
927
|
/**
|
|
641
928
|
* Begin a transaction manually
|
|
642
929
|
*/
|
|
643
|
-
begin(mode?:
|
|
644
|
-
const modeStr = mode ? ` ${mode}` :
|
|
930
|
+
begin(mode?: "DEFERRED" | "IMMEDIATE" | "EXCLUSIVE"): void {
|
|
931
|
+
const modeStr = mode ? ` ${mode}` : ""
|
|
645
932
|
this.db.run(`BEGIN${modeStr}`)
|
|
646
933
|
}
|
|
647
934
|
|
|
@@ -649,16 +936,16 @@ class DB {
|
|
|
649
936
|
* Commit a transaction
|
|
650
937
|
*/
|
|
651
938
|
commit(): void {
|
|
652
|
-
|
|
653
|
-
this.run(
|
|
939
|
+
dbLog.transaction("commit")
|
|
940
|
+
this.run("COMMIT")
|
|
654
941
|
}
|
|
655
942
|
|
|
656
943
|
/**
|
|
657
944
|
* Rollback a transaction
|
|
658
945
|
*/
|
|
659
946
|
rollback(): void {
|
|
660
|
-
|
|
661
|
-
this.run(
|
|
947
|
+
dbLog.transaction("rollback")
|
|
948
|
+
this.run("ROLLBACK")
|
|
662
949
|
}
|
|
663
950
|
|
|
664
951
|
/**
|
|
@@ -688,8 +975,10 @@ class DB {
|
|
|
688
975
|
/**
|
|
689
976
|
* Vacuum the database (reclaim space and optimize)
|
|
690
977
|
*/
|
|
691
|
-
vacuum()
|
|
692
|
-
this.db.run(
|
|
978
|
+
vacuum() {
|
|
979
|
+
const result = this.db.run("VACUUM")
|
|
980
|
+
dbLog.debug("Vacuum completed")
|
|
981
|
+
return result
|
|
693
982
|
}
|
|
694
983
|
|
|
695
984
|
/**
|
|
@@ -700,7 +989,7 @@ class DB {
|
|
|
700
989
|
const quotedName = `"${tableName.replace(/"/g, '""')}"`
|
|
701
990
|
this.db.run(`ANALYZE ${quotedName}`)
|
|
702
991
|
} else {
|
|
703
|
-
this.db.run(
|
|
992
|
+
this.db.run("ANALYZE")
|
|
704
993
|
}
|
|
705
994
|
}
|
|
706
995
|
|
|
@@ -708,7 +997,7 @@ class DB {
|
|
|
708
997
|
* Check database integrity
|
|
709
998
|
*/
|
|
710
999
|
integrityCheck(): Array<{ integrity_check: string }> {
|
|
711
|
-
const stmt = this.db.prepare(
|
|
1000
|
+
const stmt = this.db.prepare("PRAGMA integrity_check")
|
|
712
1001
|
return stmt.all() as Array<{ integrity_check: string }>
|
|
713
1002
|
}
|
|
714
1003
|
|
|
@@ -736,9 +1025,7 @@ class DB {
|
|
|
736
1025
|
dflt_value: SQLQueryBindings
|
|
737
1026
|
pk: number
|
|
738
1027
|
}> {
|
|
739
|
-
const stmt = this.db.prepare(
|
|
740
|
-
`PRAGMA table_info("${tableName.replace(/"/g, '""')}")`
|
|
741
|
-
)
|
|
1028
|
+
const stmt = this.db.prepare(`PRAGMA table_info("${tableName.replace(/"/g, '""')}")`)
|
|
742
1029
|
return stmt.all() as Array<{
|
|
743
1030
|
cid: number
|
|
744
1031
|
name: string
|
|
@@ -762,9 +1049,7 @@ class DB {
|
|
|
762
1049
|
on_delete: string
|
|
763
1050
|
match: string
|
|
764
1051
|
}> {
|
|
765
|
-
const stmt = this.db.prepare(
|
|
766
|
-
`PRAGMA foreign_key_list("${tableName.replace(/"/g, '""')}")`
|
|
767
|
-
)
|
|
1052
|
+
const stmt = this.db.prepare(`PRAGMA foreign_key_list("${tableName.replace(/"/g, '""')}")`)
|
|
768
1053
|
return stmt.all() as Array<{
|
|
769
1054
|
id: number
|
|
770
1055
|
seq: number
|
|
@@ -786,9 +1071,7 @@ class DB {
|
|
|
786
1071
|
origin: string
|
|
787
1072
|
partial: number
|
|
788
1073
|
}> {
|
|
789
|
-
const stmt = this.db.prepare(
|
|
790
|
-
`PRAGMA index_list("${tableName.replace(/"/g, '""')}")`
|
|
791
|
-
)
|
|
1074
|
+
const stmt = this.db.prepare(`PRAGMA index_list("${tableName.replace(/"/g, '""')}")`)
|
|
792
1075
|
return stmt.all() as Array<{
|
|
793
1076
|
name: string
|
|
794
1077
|
unique: number
|
|
@@ -809,10 +1092,7 @@ class DB {
|
|
|
809
1092
|
this.db.run(`PRAGMA ${name} = ${value}`)
|
|
810
1093
|
return undefined
|
|
811
1094
|
}
|
|
812
|
-
const result = this.db.prepare(`PRAGMA ${name}`).get() as Record<
|
|
813
|
-
string,
|
|
814
|
-
SQLQueryBindings
|
|
815
|
-
>
|
|
1095
|
+
const result = this.db.prepare(`PRAGMA ${name}`).get() as Record<string, SQLQueryBindings>
|
|
816
1096
|
return Object.values(result)[0]
|
|
817
1097
|
}
|
|
818
1098
|
|