@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.
- package/README.md +12 -2
- 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/{query-builder → src/query-builder}/select.ts +61 -2
- package/{query-builder → src/query-builder}/where.ts +132 -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}/update.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/README.md
CHANGED
|
@@ -12,22 +12,26 @@ Schema-first table helpers, an expressive chainable QueryBuilder, safe defaults
|
|
|
12
12
|
## 🆕 What's New in v1.3
|
|
13
13
|
|
|
14
14
|
### Bug Fixes
|
|
15
|
+
|
|
15
16
|
- **Fixed Boolean parsing** — Boolean columns now correctly convert SQLite's `0`/`1` to JavaScript `true`/`false`
|
|
16
17
|
- **Fixed Wrong packing** — Before the `publish` script was added, workspace dependencies were not correctly propagated
|
|
17
18
|
|
|
18
19
|
### New Features
|
|
20
|
+
|
|
19
21
|
- **Auto-detection of JSON & Boolean columns** — No more manual parser configuration! Columns using `column.json()` or `column.boolean()` are automatically detected from schema
|
|
20
22
|
- **Automatic backups with retention** — Configure `autoBackup` to create periodic backups with automatic cleanup of old files
|
|
21
23
|
- **Backup & Restore API** — New `backup()`, `restore()`, and `listBackups()` methods
|
|
22
24
|
- **`getPath()` method** — Get the database file path
|
|
23
25
|
|
|
24
26
|
### Architecture Improvements
|
|
27
|
+
|
|
25
28
|
- **New `utils/` module** — Reusable utilities for SQL building, logging, and row transformation
|
|
26
29
|
- **Structured logging** — Cleaner, more consistent log output with dedicated loggers per component
|
|
27
30
|
- **Reduced code duplication** — Extracted common patterns into shared utilities
|
|
28
31
|
- **Better maintainability** — Clearer separation of concerns across modules
|
|
29
32
|
|
|
30
33
|
### Breaking Changes
|
|
34
|
+
|
|
31
35
|
- None! v1.3 is fully backward compatible with v1.2.x
|
|
32
36
|
|
|
33
37
|
---
|
|
@@ -284,7 +288,10 @@ const activeAdmins = userTable
|
|
|
284
288
|
.all();
|
|
285
289
|
|
|
286
290
|
// Get first match
|
|
287
|
-
const user = userTable
|
|
291
|
+
const user = userTable
|
|
292
|
+
.select(["*"])
|
|
293
|
+
.where({ email: "alice@example.com" })
|
|
294
|
+
.first();
|
|
288
295
|
|
|
289
296
|
// Count records
|
|
290
297
|
const count = userTable.where({ active: true }).count();
|
|
@@ -321,7 +328,10 @@ userTable.insertOrIgnore({ email: "existing@example.com", name: "Name" });
|
|
|
321
328
|
userTable.insertOrReplace({ email: "existing@example.com", name: "New Name" });
|
|
322
329
|
|
|
323
330
|
// Insert and get the row back
|
|
324
|
-
const newUser = userTable.insertAndGet({
|
|
331
|
+
const newUser = userTable.insertAndGet({
|
|
332
|
+
name: "Charlie",
|
|
333
|
+
email: "charlie@example.com",
|
|
334
|
+
});
|
|
325
335
|
```
|
|
326
336
|
|
|
327
337
|
### UPDATE Operations
|
package/package.json
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dockstat/sqlite-wrapper",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
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
|
+
}
|