@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
|
@@ -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
|
+
}
|