@dockstat/sqlite-wrapper 1.3.2 → 1.3.4

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.
@@ -0,0 +1,56 @@
1
+ import { Database } from "bun:sqlite"
2
+ import type { AutoBackupOptions } from "../../index"
3
+ import { applyRetentionPolicy } from "./applyRetentionPolicy"
4
+
5
+ /**
6
+ * Create a backup of the database
7
+ *
8
+ * @param dbPath - original DB path
9
+ * @param db - Database instance
10
+ * @param backupLog - logger
11
+ * @param autoBackupOptions - if present, used to create filename and apply retention
12
+ * @param customPath - optional explicit backup path
13
+ * @returns path to created backup
14
+ */
15
+ export function backup(
16
+ dbPath: string,
17
+ db: Database,
18
+ backupLog: any,
19
+ autoBackupOptions?: AutoBackupOptions,
20
+ customPath?: string
21
+ ): string {
22
+ if (dbPath === ":memory:") {
23
+ throw new Error("Cannot backup an in-memory database")
24
+ }
25
+
26
+ const path = require("node:path")
27
+
28
+ let backupPath: string
29
+
30
+ if (customPath) {
31
+ backupPath = customPath
32
+ } else if (autoBackupOptions) {
33
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
34
+ const filename = `${autoBackupOptions.filenamePrefix}_${timestamp}.db`
35
+ backupPath = path.join(autoBackupOptions.directory, filename)
36
+ } else {
37
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
38
+ const dir = path.dirname(dbPath)
39
+ const basename = path.basename(dbPath, path.extname(dbPath))
40
+ backupPath = path.join(dir, `${basename}_backup_${timestamp}.db`)
41
+ }
42
+
43
+ try {
44
+ db.run(`VACUUM INTO '${backupPath.replace(/'/g, "''")}'`)
45
+ backupLog.backup("create", backupPath)
46
+
47
+ if (autoBackupOptions) {
48
+ applyRetentionPolicy(backupLog, autoBackupOptions)
49
+ }
50
+
51
+ return backupPath
52
+ } catch (error) {
53
+ backupLog.error(`Failed to create backup: ${error}`)
54
+ throw error
55
+ }
56
+ }
@@ -0,0 +1,44 @@
1
+ import type { AutoBackupOptions } from "../../index"
2
+
3
+ /**
4
+ * List all available backups
5
+ *
6
+ * @param autoBackupOptions - Auto-backup configuration (if not present, warns and returns empty array)
7
+ * @param backupLog - logger used for warnings/errors
8
+ * @returns Array of backup file information
9
+ */
10
+ export function listBackups(
11
+ autoBackupOptions: AutoBackupOptions | null | undefined,
12
+ backupLog: any
13
+ ): Array<{ filename: string; path: string; size: number; created: Date }> {
14
+ if (!autoBackupOptions) {
15
+ backupLog.warn("Auto-backup is not configured. Use backup() with a custom path instead.")
16
+ return []
17
+ }
18
+
19
+ const fs = require("node:fs")
20
+ const path = require("node:path")
21
+
22
+ const backupDir = autoBackupOptions.directory
23
+ const prefix = autoBackupOptions.filenamePrefix || "backup"
24
+
25
+ try {
26
+ return fs
27
+ .readdirSync(backupDir)
28
+ .filter((file: string) => file.startsWith(prefix) && file.endsWith(".db"))
29
+ .map((file: string) => {
30
+ const filePath = path.join(backupDir, file)
31
+ const stats = fs.statSync(filePath)
32
+ return {
33
+ filename: file,
34
+ path: filePath,
35
+ size: stats.size,
36
+ created: stats.mtime,
37
+ }
38
+ })
39
+ .sort((a: { created: Date }, b: { created: Date }) => b.created.getTime() - a.created.getTime())
40
+ } catch (error) {
41
+ backupLog.error(`Failed to list backups: ${error}`)
42
+ return []
43
+ }
44
+ }
@@ -0,0 +1,61 @@
1
+ import { Database } from "bun:sqlite"
2
+ import type { SqliteLogger } from "../../utils"
3
+
4
+ /**
5
+ * Restore database from a backup file
6
+ *
7
+ * Copies backupPath -> targetPath (or dbPath if targetPath omitted).
8
+ * If restoring to the same path as dbPath, closes the provided Database
9
+ * instance and reopens a new Database, returning it to the caller so the
10
+ * DB wrapper can replace its `this.db` reference.
11
+ *
12
+ * @returns Database | null — a newly opened Database instance when the restore was
13
+ * performed to the original dbPath (caller must replace its db reference);
14
+ * otherwise null.
15
+ */
16
+ export function restore(
17
+ dbPath: string,
18
+ db: Database,
19
+ backupLog: SqliteLogger,
20
+ backupPath: string,
21
+ targetPath?: string
22
+ ): Database | null {
23
+ const fs = require("node:fs")
24
+
25
+ if (!fs.existsSync(backupPath)) {
26
+ throw new Error(`Backup file not found: ${backupPath}`)
27
+ }
28
+
29
+ const restorePath = targetPath || dbPath
30
+
31
+ if (restorePath === ":memory:") {
32
+ throw new Error("Cannot restore to an in-memory database path")
33
+ }
34
+
35
+ // If restoring to the same path, close current connection first
36
+ if (restorePath === dbPath) {
37
+ try {
38
+ db.close()
39
+ } catch (err) {
40
+ // continue — if close fails, copy may still succeed or re-open will error
41
+ backupLog.warn(`Error closing DB before restore: ${err}`)
42
+ }
43
+ }
44
+
45
+ try {
46
+ fs.copyFileSync(backupPath, restorePath)
47
+ backupLog.backup("restore", backupPath)
48
+
49
+ // If we restored to the original database path, reopen and return new Database
50
+ if (restorePath === dbPath) {
51
+ const reopened = new Database(dbPath)
52
+ backupLog.info("Database connection reopened after restore")
53
+ return reopened
54
+ }
55
+
56
+ return null
57
+ } catch (error) {
58
+ backupLog.error(`Failed to restore backup: ${error}`)
59
+ throw error
60
+ }
61
+ }
@@ -0,0 +1,55 @@
1
+ import type { AutoBackupOptions } from "../../index"
2
+ import { backup } from "./backup"
3
+ import { applyRetentionPolicy } from "./applyRetentionPolicy"
4
+
5
+ export function setupAutoBackup(
6
+ dbPath: string,
7
+ db: any,
8
+ backupLog: any,
9
+ options: AutoBackupOptions
10
+ ): { timer: ReturnType<typeof setInterval> | null; autoBackupOptions: AutoBackupOptions } {
11
+ if (dbPath === ":memory:") {
12
+ backupLog.warn("Auto-backup is not available for in-memory databases")
13
+ }
14
+
15
+ const autoBackupOptions: AutoBackupOptions = {
16
+ enabled: options.enabled,
17
+ directory: options.directory,
18
+ intervalMs: options.intervalMs ?? 60 * 60 * 1000,
19
+ maxBackups: options.maxBackups ?? 10,
20
+ filenamePrefix: options.filenamePrefix ?? "backup",
21
+ compress: options.compress ?? false,
22
+ }
23
+
24
+ const fs = require("node:fs")
25
+ const path = require("node:path")
26
+
27
+ if (!fs.existsSync(autoBackupOptions.directory)) {
28
+ fs.mkdirSync(autoBackupOptions.directory, { recursive: true })
29
+ backupLog.info(`Created backup directory: ${autoBackupOptions.directory}`)
30
+ }
31
+
32
+ // Create initial backup
33
+ try {
34
+ backup(dbPath, db, backupLog, autoBackupOptions)
35
+ } catch (err) {
36
+ backupLog.error(`Initial backup failed: ${err}`)
37
+ }
38
+
39
+ // Setup interval for periodic backups
40
+ const timer = setInterval(() => {
41
+ try {
42
+ const p = backup(dbPath, db, backupLog, autoBackupOptions)
43
+ // applyRetentionPolicy already invoked inside backup, but keep safety
44
+ applyRetentionPolicy(backupLog, autoBackupOptions)
45
+ } catch (err) {
46
+ backupLog.error(`Auto-backup run failed: ${err}`)
47
+ }
48
+ }, autoBackupOptions.intervalMs)
49
+
50
+ backupLog.info(
51
+ `Auto-backup enabled: interval=${autoBackupOptions.intervalMs}ms, maxBackups=${autoBackupOptions.maxBackups}`
52
+ )
53
+
54
+ return { timer, autoBackupOptions }
55
+ }
@@ -0,0 +1,43 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { IndexColumn, IndexMethod } from "../../types"
3
+
4
+ const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
5
+
6
+ export function createIndex(
7
+ db: Database,
8
+ indexName: string,
9
+ tableName: string,
10
+ columns: IndexColumn | IndexColumn[],
11
+ options?: {
12
+ unique?: boolean
13
+ ifNotExists?: boolean
14
+ partial?: string
15
+ where?: string
16
+ using?: IndexMethod
17
+ }
18
+ ): void {
19
+ const unique = options?.unique ? "UNIQUE " : ""
20
+ const ifNot = options?.ifNotExists ? "IF NOT EXISTS " : ""
21
+ const using = options?.using ? ` USING ${options.using}` : ""
22
+
23
+ const normalizeColumn = (col: IndexColumn): string => {
24
+ if (typeof col === "string") {
25
+ return quoteIdent(col)
26
+ }
27
+
28
+ return `${quoteIdent(col.name)}${col.order ? ` ${col.order}` : ""}`
29
+ }
30
+
31
+ const columnList = Array.isArray(columns)
32
+ ? columns.map(normalizeColumn).join(", ")
33
+ : normalizeColumn(columns)
34
+
35
+ let sql = `CREATE ${unique}INDEX ${ifNot}${quoteIdent(indexName)} ON ${quoteIdent(tableName)}${using} (${columnList})`
36
+
37
+ const where = options?.where ?? options?.partial
38
+ if (where) {
39
+ sql += ` WHERE ${where}`
40
+ }
41
+
42
+ db.run(`${sql};`)
43
+ }
@@ -0,0 +1,9 @@
1
+ import type { Database } from "bun:sqlite"
2
+
3
+ const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
4
+
5
+ export function dropIndex(db: Database, indexName: string, options?: { ifExists?: boolean }): void {
6
+ const ifExists = options?.ifExists ? "IF EXISTS " : ""
7
+ const sql = `DROP INDEX ${ifExists}${quoteIdent(indexName)};`
8
+ db.run(sql)
9
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Check if a string looks like a SQL function call
3
+ */
4
+ export function isSQLFunction(str: string): boolean {
5
+ // Simple heuristic: contains parentheses and common SQL function patterns
6
+ const functionPatterns = [
7
+ /^\w+\s*\(/, // Function name followed by (
8
+ /^(datetime|date|time|strftime|current_timestamp|current_date|current_time)/i,
9
+ /^(random|abs|length|upper|lower|trim)/i,
10
+ /^(coalesce|ifnull|nullif|iif)/i,
11
+ /^(json|json_extract|json_valid)/i,
12
+ ]
13
+
14
+ return functionPatterns.some((pattern) => pattern.test(str.trim()))
15
+ }
@@ -0,0 +1,106 @@
1
+ import type { ColumnDefinition } from "../../types"
2
+ import { isSQLFunction } from "../../utils"
3
+
4
+ /**
5
+ * Build SQL column definition from ColumnDefinition object
6
+ */
7
+ export function buildColumnSQL(columnName: string, colDef: ColumnDefinition): string {
8
+ const parts: string[] = []
9
+
10
+ // Add type with optional parameters
11
+ let typeStr = colDef.type
12
+ if (colDef.length) {
13
+ typeStr += `(${colDef.length})`
14
+ } else if (colDef.precision !== undefined) {
15
+ if (colDef.scale !== undefined) {
16
+ typeStr += `(${colDef.precision}, ${colDef.scale})`
17
+ } else {
18
+ typeStr += `(${colDef.precision})`
19
+ }
20
+ }
21
+ parts.push(typeStr)
22
+
23
+ // Add PRIMARY KEY (must come before AUTOINCREMENT)
24
+ if (colDef.primaryKey) {
25
+ parts.push("PRIMARY KEY")
26
+ }
27
+
28
+ // Add AUTOINCREMENT (only valid with INTEGER PRIMARY KEY)
29
+ if (colDef.autoincrement) {
30
+ if (!colDef.type.includes("INT") || !colDef.primaryKey) {
31
+ throw new Error(
32
+ `AUTOINCREMENT can only be used with INTEGER PRIMARY KEY columns (column: ${columnName})`
33
+ )
34
+ }
35
+ parts.push("AUTOINCREMENT")
36
+ }
37
+
38
+ // Add NOT NULL (but skip if PRIMARY KEY is already specified, as it's implicit)
39
+ if (colDef.notNull && !colDef.primaryKey) {
40
+ parts.push("NOT NULL")
41
+ }
42
+
43
+ // Add UNIQUE
44
+ if (colDef.unique) {
45
+ parts.push("UNIQUE")
46
+ }
47
+
48
+ // Add DEFAULT
49
+ if (colDef.default !== undefined) {
50
+ if (colDef.default === null) {
51
+ parts.push("DEFAULT NULL")
52
+ } else if (typeof colDef.default === "object" && colDef.default._type === "expression") {
53
+ // Handle DefaultExpression
54
+ parts.push(`DEFAULT (${colDef.default.expression})`)
55
+ } else if (typeof colDef.default === "string") {
56
+ // Handle string defaults - check if it's a function call or literal
57
+ if (isSQLFunction(colDef.default)) {
58
+ parts.push(`DEFAULT (${colDef.default})`)
59
+ } else {
60
+ // Literal string value
61
+ parts.push(`DEFAULT '${colDef.default.replace(/'/g, "''")}'`)
62
+ }
63
+ } else if (typeof colDef.default === "boolean") {
64
+ parts.push(`DEFAULT ${colDef.default ? 1 : 0}`)
65
+ } else {
66
+ parts.push(`DEFAULT ${colDef.default}`)
67
+ }
68
+ }
69
+
70
+ // Add COLLATE
71
+ if (colDef.collate) {
72
+ parts.push(`COLLATE ${colDef.collate}`)
73
+ }
74
+
75
+ // Add CHECK constraint (replace placeholder with actual column name)
76
+ if (colDef.check) {
77
+ const checkConstraint = colDef.check.replace(
78
+ /\{\{COLUMN\}\}/g,
79
+ `"${columnName.replace(/"/g, '""')}"`
80
+ )
81
+ parts.push(`CHECK (${checkConstraint})`)
82
+ }
83
+
84
+ // Add REFERENCES (foreign key)
85
+ if (colDef.references) {
86
+ const ref = colDef.references
87
+ let refClause = `REFERENCES "${ref.table.replace(/"/g, '""')}"("${ref.column.replace(/"/g, '""')}")`
88
+
89
+ if (ref.onDelete) {
90
+ refClause += ` ON DELETE ${ref.onDelete}`
91
+ }
92
+
93
+ if (ref.onUpdate) {
94
+ refClause += ` ON UPDATE ${ref.onUpdate}`
95
+ }
96
+
97
+ parts.push(refClause)
98
+ }
99
+
100
+ // Add GENERATED column
101
+ if (colDef.generated) {
102
+ const storageType = colDef.generated.stored ? "STORED" : "VIRTUAL"
103
+ parts.push(`GENERATED ALWAYS AS (${colDef.generated.expression}) ${storageType}`)
104
+ }
105
+ return parts.join(" ")
106
+ }
@@ -0,0 +1,67 @@
1
+ import type { TableConstraints } from "../../types"
2
+
3
+ /**
4
+ * Build table-level constraints
5
+ */
6
+ export function buildTableConstraints<T>(constraints: TableConstraints<T>): string[] {
7
+ const parts: string[] = []
8
+
9
+ // PRIMARY KEY constraint
10
+ if (constraints.primaryKey && constraints.primaryKey.length > 0) {
11
+ const columns = constraints.primaryKey
12
+ .map((col) => `"${String(col).replace(/"/g, '""')}"`)
13
+ .join(", ")
14
+ parts.push(`PRIMARY KEY (${columns})`)
15
+ }
16
+
17
+ // UNIQUE constraints
18
+ if (constraints.unique) {
19
+ if (Array.isArray(constraints.unique[0])) {
20
+ // Multiple composite unique constraints
21
+ for (const uniqueGroup of constraints.unique as string[][]) {
22
+ const columns = uniqueGroup.map((col) => `"${col.replace(/"/g, '""')}"`).join(", ")
23
+ parts.push(`UNIQUE (${columns})`)
24
+ }
25
+ } else {
26
+ // Single unique constraint
27
+ const columns = (constraints.unique as string[])
28
+ .map((col) => `"${col.replace(/"/g, '""')}"`)
29
+ .join(", ")
30
+ parts.push(`UNIQUE (${columns})`)
31
+ }
32
+ }
33
+
34
+ // CHECK constraints
35
+ if (constraints.check) {
36
+ for (const checkExpr of constraints.check) {
37
+ parts.push(`CHECK (${checkExpr})`)
38
+ }
39
+ }
40
+
41
+ // FOREIGN KEY constraints
42
+ if (constraints.foreignKeys) {
43
+ for (const fk of constraints.foreignKeys) {
44
+ const columns = fk.columns.map((col) => `"${String(col).replace(/"/g, '""')}"`).join(", ")
45
+ const refColumns = fk.references.columns
46
+ .map((col) => `"${col.replace(/"/g, '""')}"`)
47
+ .join(", ")
48
+
49
+ let fkClause = `FOREIGN KEY (${columns}) REFERENCES "${fk.references.table.replace(
50
+ /"/g,
51
+ '""'
52
+ )}" (${refColumns})`
53
+
54
+ if (fk.references.onDelete) {
55
+ fkClause += ` ON DELETE ${fk.references.onDelete}`
56
+ }
57
+
58
+ if (fk.references.onUpdate) {
59
+ fkClause += ` ON UPDATE ${fk.references.onUpdate}`
60
+ }
61
+
62
+ parts.push(fkClause)
63
+ }
64
+ }
65
+
66
+ return parts
67
+ }
@@ -0,0 +1,16 @@
1
+ import type { Database } from "bun:sqlite"
2
+
3
+ /**
4
+ * Get table comment from metadata
5
+ */
6
+ export function getTableComment(db: Database, tableName: string): string | null {
7
+ try {
8
+ const stmt = db.prepare(`
9
+ SELECT comment FROM __table_metadata__ WHERE table_name = ?
10
+ `)
11
+ const result = stmt.get(tableName) as { comment: string } | undefined
12
+ return result?.comment || null
13
+ } catch (_error) {
14
+ return null
15
+ }
16
+ }
@@ -0,0 +1,50 @@
1
+ import type { TableSchema } from "../../types"
2
+
3
+ /**
4
+ * Type guard to determine if the provided columns object
5
+ * is a TableSchema (typed ColumnDefinition objects).
6
+ */
7
+ export function isTableSchema(columns: unknown): columns is TableSchema {
8
+ if (typeof columns !== "object" || columns === null) {
9
+ return false
10
+ }
11
+
12
+ // Check if any value has a 'type' property with a valid SQLite type
13
+ for (const [_key, value] of Object.entries(columns as Record<string, unknown>)) {
14
+ if (typeof value === "object" && value !== null && "type" in value) {
15
+ const type = (value as { type: string }).type
16
+ const validTypes = [
17
+ "INTEGER",
18
+ "TEXT",
19
+ "REAL",
20
+ "BLOB",
21
+ "NUMERIC",
22
+ "INT",
23
+ "TINYINT",
24
+ "SMALLINT",
25
+ "MEDIUMINT",
26
+ "BIGINT",
27
+ "VARCHAR",
28
+ "CHAR",
29
+ "CHARACTER",
30
+ "NCHAR",
31
+ "NVARCHAR",
32
+ "CLOB",
33
+ "DOUBLE",
34
+ "FLOAT",
35
+ "DECIMAL",
36
+ "DATE",
37
+ "DATETIME",
38
+ "TIMESTAMP",
39
+ "TIME",
40
+ "BOOLEAN",
41
+ "JSON",
42
+ ]
43
+ if (validTypes.includes(type)) {
44
+ return true
45
+ }
46
+ }
47
+ }
48
+
49
+ return false
50
+ }
@@ -0,0 +1,30 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { SqliteLogger } from "../../utils"
3
+
4
+ /**
5
+ * Store table comment as metadata (using a system table if needed)
6
+ */
7
+ export function setTableComment(
8
+ db: Database,
9
+ tableLog: SqliteLogger,
10
+ tableName: string,
11
+ comment: string
12
+ ): void {
13
+ try {
14
+ db.run(`
15
+ CREATE TABLE IF NOT EXISTS __table_metadata__ (
16
+ table_name TEXT PRIMARY KEY,
17
+ comment TEXT,
18
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
19
+ )
20
+ `)
21
+
22
+ const stmt = db.prepare(`
23
+ INSERT OR REPLACE INTO __table_metadata__ (table_name, comment, created_at)
24
+ VALUES (?, ?, CURRENT_TIMESTAMP)
25
+ `)
26
+ stmt.run(tableName, comment)
27
+ } catch (error) {
28
+ tableLog.warn(`Could not store table comment for ${tableName}: ${error}`)
29
+ }
30
+ }