@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/index.ts CHANGED
@@ -1,52 +1,49 @@
1
- import { Database, type SQLQueryBindings } from 'bun:sqlite'
2
- import { createLogger } from '@dockstat/logger'
3
- import { QueryBuilder } from './query-builder/index'
4
- import type {
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
- const logger = createLogger('sqlite-wrapper')
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
- InsertResult,
24
- UpdateResult,
20
+ ArrayKey,
21
+ ColumnConstraints,
22
+ ColumnDefinition,
23
+ ColumnNames,
24
+ DefaultExpression,
25
25
  DeleteResult,
26
+ ForeignKeyAction,
26
27
  InsertOptions,
27
- ColumnNames,
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
- } from './types'
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
- sql,
45
- SQLiteTypes,
41
+ defaultExpr,
46
42
  SQLiteFunctions,
47
43
  SQLiteKeywords,
48
- defaultExpr,
49
- } from './types'
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
- private db: Database
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: string,
74
- options?: {
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
- jsonConfig?: JsonColumnConfig<T>
353
+ parser: Partial<Parser<T>>
104
354
  ): QueryBuilder<T> {
105
- logger.debug(`Creating QueryBuilder for table: ${tableName} - JSONConfig: ${JSON.stringify(jsonConfig)}`)
106
- return new QueryBuilder<T>(this.db, tableName, jsonConfig)
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
- logger.info('Closing database connection')
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: string | Record<string, string> | Partial<Record<Extract<keyof _T, string>, ColumnDefinition>> | TableSchema,
213
- options?: TableOptions<_T>,
470
+ columns: Record<keyof _T, ColumnDefinition>,
471
+ options?: TableOptions<_T>
214
472
  ): QueryBuilder<_T> {
215
- const temp = options?.temporary ? 'TEMPORARY ' : ''
216
- const ifNot = options?.ifNotExists ? 'IF NOT EXISTS ' : ''
217
- const withoutRowId = options?.withoutRowId ? ' WITHOUT ROWID' : ''
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 (typeof columns === 'string') {
225
- // Original string-based approach
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('No columns provided')
493
+ throw new Error("No columns provided")
242
494
  }
243
- columnDefs = parts.join(', ')
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 ?? '').trim()
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('No columns provided')
516
+ throw new Error("No columns provided")
264
517
  }
265
- columnDefs = parts.join(', ')
518
+ columnDefs = parts.join(", ")
266
519
  }
267
520
 
268
- // Combine column definitions and table constraints
269
- const allDefinitions = [columnDefs, ...tableConstraints].join(', ')
521
+ const allDefinitions = [columnDefs, ...tableConstraints].join(", ")
270
522
 
271
- const sql = `CREATE ${temp}TABLE ${ifNot}${quoteIdent(tableName)} (${allDefinitions})${withoutRowId};`
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
- return this.table<_T>(tableName, options?.jsonConfig)
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 ? 'UNIQUE ' : ''
298
- const ifNot = options?.ifNotExists ? 'IF NOT EXISTS ' : ''
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(indexName)} ON ${quoteIdent(tableName)} (${columnList})`
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 ? 'IF EXISTS ' : ''
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 ? 'IF EXISTS ' : ''
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 !== 'object' || columns === null) {
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 === 'object' && value !== null && 'type' in 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
- 'INTEGER',
350
- 'TEXT',
351
- 'REAL',
352
- 'BLOB',
353
- 'NUMERIC',
354
- 'INT',
355
- 'TINYINT',
356
- 'SMALLINT',
357
- 'MEDIUMINT',
358
- 'BIGINT',
359
- 'VARCHAR',
360
- 'CHAR',
361
- 'CHARACTER',
362
- 'NCHAR',
363
- 'NVARCHAR',
364
- 'CLOB',
365
- 'DOUBLE',
366
- 'FLOAT',
367
- 'DECIMAL',
368
- 'DATE',
369
- 'DATETIME',
370
- 'TIMESTAMP',
371
- 'TIME',
372
- 'BOOLEAN',
373
- 'JSON',
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('PRIMARY KEY')
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('INT') || !colDef.primaryKey) {
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('AUTOINCREMENT')
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('NOT NULL')
711
+ parts.push("NOT NULL")
421
712
  }
422
713
 
423
714
  // Add UNIQUE
424
715
  if (colDef.unique) {
425
- parts.push('UNIQUE')
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('DEFAULT NULL')
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 === 'string') {
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 === 'boolean') {
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(/"/g, '""')}"("${ref.column.replace(/"/g, '""')}")`
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 ? 'STORED' : 'VIRTUAL'
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(/"/g, '""')}" (${refColumns})`
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
- console.warn(`Could not store table comment for ${tableName}:`, error)
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?: 'DEFERRED' | 'IMMEDIATE' | 'EXCLUSIVE'): void {
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
- logger.debug('Committing transaction')
653
- this.run('COMMIT')
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
- logger.warn('Rolling back transaction')
661
- this.run('ROLLBACK')
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(): void {
692
- this.db.run('VACUUM')
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('ANALYZE')
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('PRAGMA integrity_check')
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