@dockstat/sqlite-wrapper 1.3.2 → 1.3.3
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/package.json +7 -9
- package/src/index.ts +590 -0
- package/src/lib/backup/applyRetentionPolicy.ts +38 -0
- package/src/lib/backup/backup.ts +56 -0
- package/src/lib/backup/listBackups.ts +44 -0
- package/src/lib/backup/restore.ts +61 -0
- package/src/lib/backup/setupAutoBackup.ts +55 -0
- package/src/lib/index/createIndex.ts +43 -0
- package/src/lib/index/dropIndex.ts +9 -0
- package/src/lib/sql/isSQLFunction.ts +15 -0
- package/src/lib/table/buildColumnSQL.ts +106 -0
- package/src/lib/table/buildTableConstraint.ts +67 -0
- package/src/lib/table/getTableComment.ts +16 -0
- package/src/lib/table/isTableSchema.ts +50 -0
- package/src/lib/table/setTableComment.ts +30 -0
- package/{types.ts → src/types.ts} +11 -0
- package/index.ts +0 -1132
- /package/{query-builder → src/query-builder}/base.ts +0 -0
- /package/{query-builder → src/query-builder}/delete.ts +0 -0
- /package/{query-builder → src/query-builder}/index.ts +0 -0
- /package/{query-builder → src/query-builder}/insert.ts +0 -0
- /package/{query-builder → src/query-builder}/select.ts +0 -0
- /package/{query-builder → src/query-builder}/update.ts +0 -0
- /package/{query-builder → src/query-builder}/where.ts +0 -0
- /package/{utils → src/utils}/index.ts +0 -0
- /package/{utils → src/utils}/logger.ts +0 -0
- /package/{utils → src/utils}/sql.ts +0 -0
- /package/{utils → src/utils}/transformer.ts +0 -0
package/package.json
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dockstat/sqlite-wrapper",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "A TypeScript wrapper around bun:sqlite with type-safe query building",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./index.ts",
|
|
7
|
-
"types": "./index.ts",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./index.ts",
|
|
10
|
-
"./types": "./types.ts",
|
|
11
|
-
"./utils": "./utils/index.ts"
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./types": "./src/types.ts",
|
|
11
|
+
"./utils": "./src/utils/index.ts"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
|
-
"
|
|
15
|
-
"query-builder/**/*",
|
|
16
|
-
"utils/**/*",
|
|
14
|
+
"src",
|
|
17
15
|
"README.md",
|
|
18
16
|
"types.ts"
|
|
19
17
|
],
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import { Database, type SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
import { Logger } from "@dockstat/logger"
|
|
3
|
+
import type { ColumnDefinition, IndexColumn, IndexMethod, Parser, TableOptions } from "./types"
|
|
4
|
+
import { backup as helperBackup } from "./lib/backup/backup"
|
|
5
|
+
import { setupAutoBackup as helperSetupAutoBackup } from "./lib/backup/setupAutoBackup"
|
|
6
|
+
|
|
7
|
+
// helpers
|
|
8
|
+
import { createIndex as helperCreateIndex } from "./lib/index/createIndex"
|
|
9
|
+
import { dropIndex as helperDropIndex } from "./lib/index/dropIndex"
|
|
10
|
+
import { buildColumnSQL } from "./lib/table/buildColumnSQL"
|
|
11
|
+
import { buildTableConstraints } from "./lib/table/buildTableConstraint"
|
|
12
|
+
import { getTableComment as helperGetTableComment } from "./lib/table/getTableComment"
|
|
13
|
+
import { isTableSchema } from "./lib/table/isTableSchema"
|
|
14
|
+
import { setTableComment as helperSetTableComment } from "./lib/table/setTableComment"
|
|
15
|
+
import { QueryBuilder } from "./query-builder/index"
|
|
16
|
+
import { createLogger, type SqliteLogger } from "./utils"
|
|
17
|
+
import { listBackups as helperListBackups } from "./lib/backup/listBackups"
|
|
18
|
+
import { restore as helperRestore } from "./lib/backup/restore"
|
|
19
|
+
|
|
20
|
+
// Re-export all types and utilities
|
|
21
|
+
export { QueryBuilder }
|
|
22
|
+
export type {
|
|
23
|
+
ArrayKey,
|
|
24
|
+
ColumnConstraints,
|
|
25
|
+
ColumnDefinition,
|
|
26
|
+
ColumnNames,
|
|
27
|
+
DefaultExpression,
|
|
28
|
+
DeleteResult,
|
|
29
|
+
ForeignKeyAction,
|
|
30
|
+
InsertOptions,
|
|
31
|
+
InsertResult,
|
|
32
|
+
RegexCondition,
|
|
33
|
+
SQLiteType,
|
|
34
|
+
TableConstraints,
|
|
35
|
+
TableOptions,
|
|
36
|
+
TableSchema,
|
|
37
|
+
UpdateResult,
|
|
38
|
+
WhereCondition,
|
|
39
|
+
} from "./types"
|
|
40
|
+
|
|
41
|
+
// Re-export helper utilities
|
|
42
|
+
export {
|
|
43
|
+
column,
|
|
44
|
+
defaultExpr,
|
|
45
|
+
SQLiteFunctions,
|
|
46
|
+
SQLiteKeywords,
|
|
47
|
+
SQLiteTypes,
|
|
48
|
+
sql,
|
|
49
|
+
} from "./types"
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Auto-backup configuration options
|
|
53
|
+
*/
|
|
54
|
+
export interface AutoBackupOptions {
|
|
55
|
+
/** Enable automatic backups */
|
|
56
|
+
enabled: boolean
|
|
57
|
+
/** Directory to store backup files */
|
|
58
|
+
directory: string
|
|
59
|
+
/** Backup interval in milliseconds (default: 1 hour) */
|
|
60
|
+
intervalMs?: number
|
|
61
|
+
/** Maximum number of backups to retain (default: 10) */
|
|
62
|
+
maxBackups?: number
|
|
63
|
+
/** Prefix for backup filenames (default: 'backup') */
|
|
64
|
+
filenamePrefix?: string
|
|
65
|
+
/** Whether to compress backups using gzip (default: false) */
|
|
66
|
+
compress?: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Database configuration options
|
|
71
|
+
*/
|
|
72
|
+
export interface DBOptions {
|
|
73
|
+
/** PRAGMA settings to apply on database open */
|
|
74
|
+
pragmas?: Array<[string, SQLQueryBindings]>
|
|
75
|
+
/** Paths to SQLite extensions to load */
|
|
76
|
+
loadExtensions?: string[]
|
|
77
|
+
/** Auto-backup configuration */
|
|
78
|
+
autoBackup?: AutoBackupOptions
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class DB {
|
|
82
|
+
protected db: Database
|
|
83
|
+
protected dbPath: string
|
|
84
|
+
private autoBackupTimer: ReturnType<typeof setInterval> | null = null
|
|
85
|
+
private autoBackupOptions: AutoBackupOptions | null = null
|
|
86
|
+
private baseLogger: Logger
|
|
87
|
+
private dbLog: SqliteLogger
|
|
88
|
+
private backupLog: SqliteLogger
|
|
89
|
+
private tableLog: SqliteLogger
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Open or create a SQLite database at `path`.
|
|
93
|
+
*
|
|
94
|
+
* @param path - Path to the SQLite file (e.g. "app.db"). Use ":memory:" for in-memory DB.
|
|
95
|
+
* @param options - Optional database configuration
|
|
96
|
+
*/
|
|
97
|
+
constructor(path: string, options?: DBOptions, baseLogger?: Logger) {
|
|
98
|
+
if (!baseLogger) {
|
|
99
|
+
this.baseLogger = new Logger("Sqlite-Wrapper")
|
|
100
|
+
} else {
|
|
101
|
+
this.baseLogger = baseLogger
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Wire base logger so sqlite-wrapper logs inherit the same LogHook/parents as the consumer.
|
|
105
|
+
this.dbLog = createLogger("DB", this.baseLogger)
|
|
106
|
+
this.backupLog = createLogger("Backup", this.baseLogger)
|
|
107
|
+
this.tableLog = createLogger("Table", this.baseLogger)
|
|
108
|
+
|
|
109
|
+
this.dbLog.connection(path, "open")
|
|
110
|
+
|
|
111
|
+
this.dbPath = path
|
|
112
|
+
this.db = new Database(path)
|
|
113
|
+
|
|
114
|
+
// Apply PRAGMA settings if provided
|
|
115
|
+
if (options?.pragmas) {
|
|
116
|
+
for (const [name, value] of options.pragmas) {
|
|
117
|
+
this.pragma(name, value)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Load extensions if provided
|
|
122
|
+
if (options?.loadExtensions) {
|
|
123
|
+
for (const extensionPath of options.loadExtensions) {
|
|
124
|
+
this.loadExtension(extensionPath)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Setup auto-backup if configured
|
|
129
|
+
if (options?.autoBackup?.enabled) {
|
|
130
|
+
const res = helperSetupAutoBackup(this.dbPath, this.db, this.backupLog, options.autoBackup)
|
|
131
|
+
this.autoBackupTimer = res.timer
|
|
132
|
+
this.autoBackupOptions = res.autoBackupOptions
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* List all available backups
|
|
138
|
+
*
|
|
139
|
+
* @returns Array of backup file information
|
|
140
|
+
*/
|
|
141
|
+
listBackups(): Array<{ filename: string; path: string; size: number; created: Date }> {
|
|
142
|
+
return helperListBackups(this.autoBackupOptions, this.backupLog)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create a backup of the database
|
|
147
|
+
*
|
|
148
|
+
* @param customPath - Optional custom path for the backup file. If not provided, uses auto-backup settings or generates a timestamped filename.
|
|
149
|
+
* @returns The path to the created backup file
|
|
150
|
+
*/
|
|
151
|
+
backup(customPath?: string): string {
|
|
152
|
+
return helperBackup(
|
|
153
|
+
this.dbPath,
|
|
154
|
+
this.db,
|
|
155
|
+
this.backupLog,
|
|
156
|
+
this.autoBackupOptions ?? undefined,
|
|
157
|
+
customPath
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Stop auto-backup if it's running
|
|
163
|
+
*/
|
|
164
|
+
stopAutoBackup(): void {
|
|
165
|
+
if (this.autoBackupTimer) {
|
|
166
|
+
clearInterval(this.autoBackupTimer)
|
|
167
|
+
this.autoBackupTimer = null
|
|
168
|
+
this.backupLog.info("Auto-backup stopped")
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the database file path
|
|
174
|
+
*/
|
|
175
|
+
getPath(): string {
|
|
176
|
+
return this.dbPath
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get a typed QueryBuilder for a given table name.
|
|
181
|
+
*/
|
|
182
|
+
table<T extends Record<string, unknown>>(
|
|
183
|
+
tableName: string,
|
|
184
|
+
parser: Partial<Parser<T>> = {}
|
|
185
|
+
): QueryBuilder<T> {
|
|
186
|
+
const pObj: Parser<T> = {
|
|
187
|
+
JSON: parser.JSON || [],
|
|
188
|
+
MODULE: parser.MODULE || {},
|
|
189
|
+
BOOLEAN: parser.BOOLEAN || [],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.tableLog.debug(`Creating QueryBuilder for: ${tableName}`)
|
|
193
|
+
return new QueryBuilder<T>(this.db, tableName, pObj, this.baseLogger)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Close the underlying SQLite database handle.
|
|
198
|
+
* Also stops auto-backup if it's running.
|
|
199
|
+
*/
|
|
200
|
+
close(): void {
|
|
201
|
+
this.dbLog.connection(this.dbPath, "close")
|
|
202
|
+
this.stopAutoBackup()
|
|
203
|
+
this.db.close()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
restore(backupPath: string, targetPath?: string): void {
|
|
207
|
+
const reopened = helperRestore(this.dbPath, this.db, this.backupLog, backupPath, targetPath)
|
|
208
|
+
if (reopened instanceof Database) {
|
|
209
|
+
this.db = reopened
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a table with comprehensive type safety and feature support.
|
|
215
|
+
*
|
|
216
|
+
* @param tableName - Table name to create.
|
|
217
|
+
* @param columns - Column definitions (string, legacy object, or type-safe schema).
|
|
218
|
+
* @param options - Table options including constraints and metadata.
|
|
219
|
+
*
|
|
220
|
+
* @throws {Error} If column definitions are invalid or constraints conflict.
|
|
221
|
+
*/
|
|
222
|
+
createTable<_T extends Record<string, unknown> = Record<string, unknown>>(
|
|
223
|
+
tableName: string,
|
|
224
|
+
columns: Record<keyof _T, ColumnDefinition>,
|
|
225
|
+
options?: TableOptions<_T>
|
|
226
|
+
): QueryBuilder<_T> {
|
|
227
|
+
const temp = options?.temporary ? "TEMPORARY " : tableName === ":memory" ? "TEMPORARY " : ""
|
|
228
|
+
const ifNot = options?.ifNotExists ? "IF NOT EXISTS " : ""
|
|
229
|
+
const withoutRowId = options?.withoutRowId ? " WITHOUT ROWID" : ""
|
|
230
|
+
|
|
231
|
+
const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
|
|
232
|
+
|
|
233
|
+
let columnDefs: string
|
|
234
|
+
let tableConstraints: string[] = []
|
|
235
|
+
|
|
236
|
+
if (isTableSchema(columns)) {
|
|
237
|
+
// comprehensive type-safe approach
|
|
238
|
+
const parts: string[] = []
|
|
239
|
+
for (const [colName, colDef] of Object.entries(columns)) {
|
|
240
|
+
if (!colName) continue
|
|
241
|
+
|
|
242
|
+
const sqlDef = buildColumnSQL(colName, colDef)
|
|
243
|
+
parts.push(`${quoteIdent(colName)} ${sqlDef}`)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (parts.length === 0) {
|
|
247
|
+
throw new Error("No columns provided")
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
columnDefs = parts.join(", ")
|
|
251
|
+
|
|
252
|
+
// Add table-level constraints
|
|
253
|
+
if (options?.constraints) {
|
|
254
|
+
tableConstraints = buildTableConstraints(options.constraints)
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
// Original object-based approach
|
|
258
|
+
const parts: string[] = []
|
|
259
|
+
for (const [col, def] of Object.entries(columns)) {
|
|
260
|
+
if (!col) continue
|
|
261
|
+
|
|
262
|
+
const defTrim = def || ""
|
|
263
|
+
if (!defTrim) {
|
|
264
|
+
throw new Error(`Missing SQL type/constraints for column "${col}"`)
|
|
265
|
+
}
|
|
266
|
+
parts.push(`${quoteIdent(col)} ${defTrim}`)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (parts.length === 0) {
|
|
270
|
+
throw new Error("No columns provided")
|
|
271
|
+
}
|
|
272
|
+
columnDefs = parts.join(", ")
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const allDefinitions = [columnDefs, ...tableConstraints].join(", ")
|
|
276
|
+
|
|
277
|
+
const columnNames = Object.keys(columns)
|
|
278
|
+
this.tableLog.tableCreate(tableName, columnNames)
|
|
279
|
+
|
|
280
|
+
const sql = `CREATE ${temp}TABLE ${ifNot}${quoteIdent(
|
|
281
|
+
tableName
|
|
282
|
+
)} (${allDefinitions})${withoutRowId};`
|
|
283
|
+
|
|
284
|
+
this.db.run(sql)
|
|
285
|
+
|
|
286
|
+
// Store table comment as metadata if provided
|
|
287
|
+
if (options?.comment) {
|
|
288
|
+
helperSetTableComment(this.db, this.tableLog, tableName, options.comment)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Auto-detect JSON and BOOLEAN columns from schema
|
|
292
|
+
const autoDetectedJson: Array<keyof _T> = []
|
|
293
|
+
const autoDetectedBoolean: Array<keyof _T> = []
|
|
294
|
+
|
|
295
|
+
if (isTableSchema(columns)) {
|
|
296
|
+
for (const [colName, colDef] of Object.entries(columns)) {
|
|
297
|
+
if (colDef.type === "JSON") {
|
|
298
|
+
autoDetectedJson.push(colName as keyof _T)
|
|
299
|
+
}
|
|
300
|
+
if (colDef.type === "BOOLEAN") {
|
|
301
|
+
autoDetectedBoolean.push(colName as keyof _T)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Merge auto-detected columns with user-provided parser options
|
|
307
|
+
const userJson = options?.parser?.JSON || []
|
|
308
|
+
const userBoolean = options?.parser?.BOOLEAN || []
|
|
309
|
+
const userModule = options?.parser?.MODULE || {}
|
|
310
|
+
|
|
311
|
+
// Combine and deduplicate
|
|
312
|
+
const mergedJson = [...new Set([...autoDetectedJson, ...userJson])] as Array<keyof _T>
|
|
313
|
+
const mergedBoolean = [...new Set([...autoDetectedBoolean, ...userBoolean])] as Array<keyof _T>
|
|
314
|
+
|
|
315
|
+
const pObj = {
|
|
316
|
+
JSON: mergedJson,
|
|
317
|
+
MODULE: userModule,
|
|
318
|
+
BOOLEAN: mergedBoolean,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.tableLog.parserConfig(
|
|
322
|
+
pObj.JSON.map(String),
|
|
323
|
+
pObj.BOOLEAN.map(String),
|
|
324
|
+
Object.keys(pObj.MODULE)
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return this.table<_T>(tableName, pObj)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Create an index on a table
|
|
332
|
+
*/
|
|
333
|
+
createIndex(
|
|
334
|
+
indexName: string,
|
|
335
|
+
tableName: string,
|
|
336
|
+
columns: IndexColumn | IndexColumn[],
|
|
337
|
+
options?: {
|
|
338
|
+
unique?: boolean
|
|
339
|
+
ifNotExists?: boolean
|
|
340
|
+
/** Alias for `where` */
|
|
341
|
+
partial?: string
|
|
342
|
+
/** WHERE clause for partial indexes */
|
|
343
|
+
where?: string
|
|
344
|
+
/** Index method (USING ...) */
|
|
345
|
+
using?: IndexMethod
|
|
346
|
+
}
|
|
347
|
+
): void {
|
|
348
|
+
helperCreateIndex(this.db, indexName, tableName, columns, options)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Drop a table
|
|
353
|
+
*/
|
|
354
|
+
dropTable(tableName: string, options?: { ifExists?: boolean }): void {
|
|
355
|
+
const ifExists = options?.ifExists ? "IF EXISTS " : ""
|
|
356
|
+
const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
|
|
357
|
+
const sql = `DROP TABLE ${ifExists}${quoteIdent(tableName)};`
|
|
358
|
+
this.db.run(sql)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Drop an index
|
|
363
|
+
*/
|
|
364
|
+
dropIndex(indexName: string, options?: { ifExists?: boolean }): void {
|
|
365
|
+
helperDropIndex(this.db, indexName, options)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Store table comment as metadata (using a system table if needed)
|
|
370
|
+
*/
|
|
371
|
+
getTableComment(tableName: string): string | null {
|
|
372
|
+
return helperGetTableComment(this.db, tableName)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* runute a raw SQL statement
|
|
377
|
+
*/
|
|
378
|
+
run(sql: string): void {
|
|
379
|
+
this.tableLog.debug(`Running SQL: ${sql}`)
|
|
380
|
+
this.db.run(sql)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Prepare a SQL statement for repeated runution
|
|
385
|
+
*/
|
|
386
|
+
prepare(sql: string) {
|
|
387
|
+
return this.db.prepare(sql)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* runute a transaction
|
|
392
|
+
*/
|
|
393
|
+
transaction<T>(fn: () => T): T {
|
|
394
|
+
return this.db.transaction(fn)()
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Begin a transaction manually
|
|
399
|
+
*/
|
|
400
|
+
begin(mode?: "DEFERRED" | "IMMEDIATE" | "EXCLUSIVE"): void {
|
|
401
|
+
const modeStr = mode ? ` ${mode}` : ""
|
|
402
|
+
this.db.run(`BEGIN${modeStr}`)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Commit a transaction
|
|
407
|
+
*/
|
|
408
|
+
commit(): void {
|
|
409
|
+
this.dbLog.transaction("commit")
|
|
410
|
+
this.run("COMMIT")
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Rollback a transaction
|
|
415
|
+
*/
|
|
416
|
+
rollback(): void {
|
|
417
|
+
this.dbLog.transaction("rollback")
|
|
418
|
+
this.run("ROLLBACK")
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Create a savepoint
|
|
423
|
+
*/
|
|
424
|
+
savepoint(name: string): void {
|
|
425
|
+
const quotedName = `"${name.replace(/"/g, '""')}"`
|
|
426
|
+
this.db.run(`SAVEPOINT ${quotedName}`)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Release a savepoint
|
|
431
|
+
*/
|
|
432
|
+
releaseSavepoint(name: string): void {
|
|
433
|
+
const quotedName = `"${name.replace(/"/g, '""')}"`
|
|
434
|
+
this.db.run(`RELEASE SAVEPOINT ${quotedName}`)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Rollback to a savepoint
|
|
439
|
+
*/
|
|
440
|
+
rollbackToSavepoint(name: string): void {
|
|
441
|
+
const quotedName = `"${name.replace(/"/g, '""')}"`
|
|
442
|
+
this.db.run(`ROLLBACK TO SAVEPOINT ${quotedName}`)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Vacuum the database (reclaim space and optimize)
|
|
447
|
+
*/
|
|
448
|
+
vacuum() {
|
|
449
|
+
const result = this.db.run("VACUUM")
|
|
450
|
+
this.dbLog.debug("Vacuum completed")
|
|
451
|
+
return result
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Analyze the database (update statistics for query optimizer)
|
|
456
|
+
*/
|
|
457
|
+
analyze(tableName?: string): void {
|
|
458
|
+
if (tableName) {
|
|
459
|
+
const quotedName = `"${tableName.replace(/"/g, '""')}"`
|
|
460
|
+
this.db.run(`ANALYZE ${quotedName}`)
|
|
461
|
+
} else {
|
|
462
|
+
this.db.run("ANALYZE")
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Check database integrity
|
|
468
|
+
*/
|
|
469
|
+
integrityCheck(): Array<{ integrity_check: string }> {
|
|
470
|
+
const stmt = this.db.prepare("PRAGMA integrity_check")
|
|
471
|
+
return stmt.all() as Array<{ integrity_check: string }>
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get database schema information
|
|
476
|
+
*/
|
|
477
|
+
getSchema(): Array<{ name: string; type: string; sql: string }> {
|
|
478
|
+
const stmt = this.db.prepare(`
|
|
479
|
+
SELECT name, type, sql
|
|
480
|
+
FROM sqlite_master
|
|
481
|
+
WHERE type IN ('table', 'index', 'view', 'trigger')
|
|
482
|
+
ORDER BY type, name
|
|
483
|
+
`)
|
|
484
|
+
return stmt.all() as Array<{ name: string; type: string; sql: string }>
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get table info (columns, types, constraints)
|
|
489
|
+
*/
|
|
490
|
+
getTableInfo(tableName: string): Array<{
|
|
491
|
+
cid: number
|
|
492
|
+
name: string
|
|
493
|
+
type: string
|
|
494
|
+
notnull: number
|
|
495
|
+
dflt_value: SQLQueryBindings
|
|
496
|
+
pk: number
|
|
497
|
+
}> {
|
|
498
|
+
const stmt = this.db.prepare(`PRAGMA table_info("${tableName.replace(/"/g, '""')}")`)
|
|
499
|
+
return stmt.all() as Array<{
|
|
500
|
+
cid: number
|
|
501
|
+
name: string
|
|
502
|
+
type: string
|
|
503
|
+
notnull: number
|
|
504
|
+
dflt_value: SQLQueryBindings
|
|
505
|
+
pk: number
|
|
506
|
+
}>
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get foreign key information for a table
|
|
511
|
+
*/
|
|
512
|
+
getForeignKeys(tableName: string): Array<{
|
|
513
|
+
id: number
|
|
514
|
+
seq: number
|
|
515
|
+
table: string
|
|
516
|
+
from: string
|
|
517
|
+
to: string
|
|
518
|
+
on_update: string
|
|
519
|
+
on_delete: string
|
|
520
|
+
match: string
|
|
521
|
+
}> {
|
|
522
|
+
const stmt = this.db.prepare(`PRAGMA foreign_key_list("${tableName.replace(/"/g, '""')}")`)
|
|
523
|
+
return stmt.all() as Array<{
|
|
524
|
+
id: number
|
|
525
|
+
seq: number
|
|
526
|
+
table: string
|
|
527
|
+
from: string
|
|
528
|
+
to: string
|
|
529
|
+
on_update: string
|
|
530
|
+
on_delete: string
|
|
531
|
+
match: string
|
|
532
|
+
}>
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Get index information for a table
|
|
537
|
+
*/
|
|
538
|
+
getIndexes(tableName: string): Array<{
|
|
539
|
+
name: string
|
|
540
|
+
unique: number
|
|
541
|
+
origin: string
|
|
542
|
+
partial: number
|
|
543
|
+
}> {
|
|
544
|
+
const stmt = this.db.prepare(`PRAGMA index_list("${tableName.replace(/"/g, '""')}")`)
|
|
545
|
+
return stmt.all() as Array<{
|
|
546
|
+
name: string
|
|
547
|
+
unique: number
|
|
548
|
+
origin: string
|
|
549
|
+
partial: number
|
|
550
|
+
}>
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Set or get a PRAGMA value.
|
|
555
|
+
*
|
|
556
|
+
* @param name - PRAGMA name (e.g., "foreign_keys", "journal_mode")
|
|
557
|
+
* @param value - Value to set (omit to get current value)
|
|
558
|
+
* @returns Current value when getting, undefined when setting
|
|
559
|
+
*/
|
|
560
|
+
pragma(name: string, value?: SQLQueryBindings): SQLQueryBindings | undefined {
|
|
561
|
+
if (value !== undefined) {
|
|
562
|
+
this.db.run(`PRAGMA ${name} = ${value}`)
|
|
563
|
+
return undefined
|
|
564
|
+
}
|
|
565
|
+
const result = this.db.prepare(`PRAGMA ${name}`).get() as Record<string, SQLQueryBindings>
|
|
566
|
+
return Object.values(result)[0]
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Load a SQLite extension.
|
|
571
|
+
*
|
|
572
|
+
* @param path - Absolute path to the compiled SQLite extension
|
|
573
|
+
*/
|
|
574
|
+
loadExtension(path: string): void {
|
|
575
|
+
this.db.loadExtension(path)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get direct access to the underlying SQLite database instance.
|
|
580
|
+
* Use this for advanced operations not covered by the wrapper.
|
|
581
|
+
*
|
|
582
|
+
* @returns The underlying Database instance
|
|
583
|
+
*/
|
|
584
|
+
getDb(): Database {
|
|
585
|
+
return this.db
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export { DB }
|
|
590
|
+
export default DB
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AutoBackupOptions } from "../../index"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply retention policy to remove old backups
|
|
5
|
+
*/
|
|
6
|
+
export function applyRetentionPolicy(backupLog: any, autoBackupOptions: AutoBackupOptions): void {
|
|
7
|
+
if (!autoBackupOptions) return
|
|
8
|
+
|
|
9
|
+
const fs = require("node:fs")
|
|
10
|
+
const path = require("node:path")
|
|
11
|
+
|
|
12
|
+
const backupDir = autoBackupOptions.directory
|
|
13
|
+
const prefix = autoBackupOptions.filenamePrefix || "backup"
|
|
14
|
+
const maxBackups = autoBackupOptions.maxBackups || 10
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const files = fs
|
|
18
|
+
.readdirSync(backupDir)
|
|
19
|
+
.filter((file: string) => file.startsWith(prefix) && file.endsWith(".db"))
|
|
20
|
+
.map((file: string) => ({
|
|
21
|
+
name: file,
|
|
22
|
+
path: path.join(backupDir, file),
|
|
23
|
+
mtime: fs.statSync(path.join(backupDir, file)).mtime.getTime(),
|
|
24
|
+
}))
|
|
25
|
+
.sort((a: { mtime: number }, b: { mtime: number }) => b.mtime - a.mtime) // newest first
|
|
26
|
+
|
|
27
|
+
if (files.length > maxBackups) {
|
|
28
|
+
const toDelete = files.slice(maxBackups)
|
|
29
|
+
for (const file of toDelete) {
|
|
30
|
+
fs.unlinkSync(file.path)
|
|
31
|
+
backupLog.debug(`Removed old backup: ${file.name}`)
|
|
32
|
+
}
|
|
33
|
+
backupLog.info(`Retention policy applied: removed ${toDelete.length} old backup(s)`)
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
backupLog.error(`Failed to apply retention policy: ${error}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -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
|
+
}
|