@dockstat/sqlite-wrapper 1.3.5 → 1.3.6

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 CHANGED
@@ -22,7 +22,7 @@ Schema-first table helpers, an expressive chainable QueryBuilder, safe defaults
22
22
  ### Bug Fixes
23
23
 
24
24
  - **Fixed Boolean parsing** — Boolean columns now correctly convert SQLite's `0`/`1` to JavaScript `true`/`false`
25
- - **Fixed Wrong packing** — Before the `publish` script was added, workspace dependencies were not correctly propagated
25
+ - **Fixed incorrect packaging** — Before the `publish` script was added, workspace dependencies were not correctly propagated
26
26
 
27
27
  ### Architecture Improvements
28
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dockstat/sqlite-wrapper",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "A TypeScript wrapper around bun:sqlite with type-safe query building",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/index.ts CHANGED
@@ -18,9 +18,9 @@ import type {
18
18
  ColumnDefinition,
19
19
  IndexColumn,
20
20
  IndexMethod,
21
+ MigrationOptions,
21
22
  Parser,
22
23
  TableOptions,
23
- MigrationOptions,
24
24
  } from "./types"
25
25
  import { createLogger, type SqliteLogger } from "./utils"
26
26
 
@@ -220,6 +220,23 @@ class DB {
220
220
  }
221
221
  }
222
222
 
223
+ private normalizeMigrationOptions(migrate: TableOptions<Record<string, unknown>>["migrate"]): {
224
+ enabled: boolean
225
+ options: MigrationOptions
226
+ } {
227
+ if (migrate === false) return { enabled: false, options: {} }
228
+ if (migrate && typeof migrate === "object") {
229
+ return { enabled: true, options: migrate }
230
+ }
231
+ return { enabled: true, options: {} }
232
+ }
233
+
234
+ private shouldCheckExistingTable<_T>(tableName: string, options?: TableOptions<_T>): boolean {
235
+ if (options?.temporary) return false
236
+ if (tableName === ":memory:") return false
237
+ return true
238
+ }
239
+
223
240
  /**
224
241
  * Create a table with comprehensive type safety and feature support.
225
242
  *
@@ -234,20 +251,15 @@ class DB {
234
251
  columns: Record<keyof _T, ColumnDefinition>,
235
252
  options?: TableOptions<_T>
236
253
  ): QueryBuilder<_T> {
237
- // Handle migration if enabled (default: true)
238
- const migrateOption = options?.migrate !== undefined ? options.migrate : true
254
+ const { enabled: migrateEnabled, options: migrationOptions } = this.normalizeMigrationOptions(
255
+ options?.migrate
256
+ )
239
257
 
240
- // Check if table already exists
241
258
  const tableAlreadyExists =
242
- !options?.temporary && tableName !== ":memory:" && tableExists(this.db, tableName)
259
+ this.shouldCheckExistingTable(tableName, options) && tableExists(this.db, tableName)
243
260
 
244
261
  if (tableAlreadyExists) {
245
- if (migrateOption !== false) {
246
- // Migration is enabled, check if we need to migrate
247
- const migrationOptions: MigrationOptions =
248
- typeof migrateOption === "object" ? migrateOption : {}
249
-
250
- // Build table constraints to pass to migration
262
+ if (migrateEnabled) {
251
263
  let tableConstraints: string[] = []
252
264
  if (isTableSchema(columns) && options?.constraints) {
253
265
  tableConstraints = buildTableConstraints(options.constraints)
@@ -263,21 +275,16 @@ class DB {
263
275
  )
264
276
 
265
277
  if (migrated) {
266
- // Table was migrated, skip creation and go directly to parser setup
267
278
  return this._setupTableParser(tableName, columns, options)
268
279
  }
269
280
  }
270
281
 
271
- // Table exists and either:
272
- // 1. Migration is disabled, or
273
- // 2. Migration is enabled but no changes were needed
274
- // In both cases, return the existing table without trying to create it
275
282
  if (!options?.ifNotExists) {
276
283
  return this._setupTableParser(tableName, columns, options)
277
284
  }
278
285
  }
279
286
 
280
- const temp = options?.temporary ? "TEMPORARY " : tableName === ":memory" ? "TEMPORARY " : ""
287
+ const temp = options?.temporary ? "TEMPORARY " : tableName === ":memory:" ? "TEMPORARY " : ""
281
288
  const ifNot = options?.ifNotExists ? "IF NOT EXISTS " : ""
282
289
  const withoutRowId = options?.withoutRowId ? " WITHOUT ROWID" : ""
283
290
 
package/src/migration.ts CHANGED
@@ -1,36 +1,15 @@
1
1
  import type { Database } from "bun:sqlite"
2
- import type { ColumnDefinition, MigrationOptions } from "./types"
3
2
  import { buildColumnSQL } from "./lib/table/buildColumnSQL"
3
+ import type {
4
+ ColumnDefinition,
5
+ ForeignKeyInfo,
6
+ ForeignKeyStatus,
7
+ IndexInfo,
8
+ MigrationOptions,
9
+ TableColumn,
10
+ } from "./types"
4
11
  import { SqliteLogger } from "./utils/logger"
5
12
 
6
- export interface TableColumn {
7
- cid: number
8
- name: string
9
- type: string
10
- notnull: number
11
- dflt_value: string | number | null
12
- pk: number
13
- }
14
-
15
- export interface IndexInfo {
16
- name: string
17
- sql: string | null
18
- unique: boolean
19
- origin: string
20
- partial: number
21
- }
22
-
23
- export interface ForeignKeyInfo {
24
- id: number
25
- seq: number
26
- table: string
27
- from: string
28
- to: string
29
- on_update: string
30
- on_delete: string
31
- match: string
32
- }
33
-
34
13
  /**
35
14
  * Get the current schema of a table
36
15
  */
@@ -59,10 +38,6 @@ export function getTableIndexes(db: Database, tableName: string): IndexInfo[] {
59
38
  const indexes = stmt.all() as PragmaIndex[]
60
39
 
61
40
  return indexes.map((idx) => {
62
- const infoStmt = db.prepare(`PRAGMA index_info("${idx.name}")`)
63
- infoStmt.all() // Execute but don't need the result for this use case
64
-
65
- // Get the CREATE INDEX statement if available
66
41
  const sqlStmt = db.prepare(`SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?`)
67
42
  const sqlResult = sqlStmt.get(idx.name) as SqliteMasterRow | null
68
43
 
@@ -120,7 +95,9 @@ export function schemasAreDifferent(
120
95
 
121
96
  // Check if column count differs
122
97
  if (currentColumnNames.size !== newColumnNames.size) {
123
- logger.info("Column count differs")
98
+ logger.info(
99
+ `Column count differs (current: ${currentColumnNames.size} [${Array.from(currentColumnNames).join(", ")}], new: ${newColumnNames.size} [${Array.from(newColumnNames).join(", ")}])`
100
+ )
124
101
  return true
125
102
  }
126
103
 
@@ -229,26 +206,31 @@ export function migrateTable(
229
206
  newColumns: Record<string, ColumnDefinition>,
230
207
  options: MigrationOptions = {},
231
208
  tableConstraints: string[] = [],
232
- migrationLog: SqliteLogger
209
+ migrationLog: SqliteLogger,
210
+ currentSchema?: TableColumn[]
233
211
  ): void {
234
212
  const { preserveData = true, onConflict = "fail", tempTableSuffix = "_migration_temp" } = options
235
213
 
236
214
  migrationLog.info(`Starting migration for table: ${tableName}`)
237
215
 
238
216
  // Get current table info
239
- const currentSchema = getTableColumns(db, tableName)
217
+ const effectiveSchema = currentSchema ?? getTableColumns(db, tableName)
240
218
  const indexes = getTableIndexes(db, tableName)
241
219
  const triggers = getTableTriggers(db, tableName)
242
220
 
243
221
  // Check if migration is needed
244
- if (!schemasAreDifferent(currentSchema, newColumns, migrationLog)) {
222
+ if (!schemasAreDifferent(effectiveSchema, newColumns, migrationLog)) {
245
223
  migrationLog.info(`No migration needed for table: ${tableName}`)
246
224
  return
247
225
  }
248
226
 
249
227
  const tempTableName = `${tableName}${tempTableSuffix}`
250
228
 
251
- // Start transaction for atomic migration
229
+ const fkStatus = db.prepare("PRAGMA foreign_keys").get() as ForeignKeyStatus | null
230
+ if (fkStatus && fkStatus.foreign_keys === 1) {
231
+ db.run("PRAGMA foreign_keys = OFF")
232
+ }
233
+
252
234
  db.transaction(() => {
253
235
  try {
254
236
  // Step 1: Create temporary table with new schema
@@ -270,8 +252,8 @@ export function migrateTable(
270
252
  db.run(createTempTableSql)
271
253
 
272
254
  // Step 2: Copy data if requested
273
- if (preserveData && currentSchema.length > 0) {
274
- const { selectColumns, insertColumns } = generateColumnMapping(currentSchema, newColumns)
255
+ if (preserveData && effectiveSchema.length > 0) {
256
+ const { selectColumns, insertColumns } = generateColumnMapping(effectiveSchema, newColumns)
275
257
 
276
258
  if (selectColumns.length > 0) {
277
259
  migrationLog.debug(`Copying data from ${tableName} to ${tempTableName}`)
@@ -297,15 +279,6 @@ export function migrateTable(
297
279
  // Step 3: Drop the original table
298
280
  migrationLog.debug(`Dropping original table: ${tableName}`)
299
281
 
300
- // Temporarily disable foreign key constraints
301
- interface ForeignKeyStatus {
302
- foreign_keys: number
303
- }
304
- const fkStatus = db.prepare("PRAGMA foreign_keys").get() as ForeignKeyStatus | null
305
- if (fkStatus && fkStatus.foreign_keys === 1) {
306
- db.run("PRAGMA foreign_keys = OFF")
307
- }
308
-
309
282
  db.run(`DROP TABLE "${tableName}"`)
310
283
 
311
284
  // Step 4: Rename temporary table to original name
@@ -335,9 +308,6 @@ export function migrateTable(
335
308
  }
336
309
 
337
310
  // Re-enable foreign key constraints if they were enabled
338
- if (fkStatus && fkStatus.foreign_keys === 1) {
339
- db.run("PRAGMA foreign_keys = ON")
340
- }
341
311
 
342
312
  migrationLog.info(`Successfully migrated table: ${tableName}`)
343
313
  } catch (error) {
@@ -345,6 +315,10 @@ export function migrateTable(
345
315
  throw error
346
316
  }
347
317
  })()
318
+
319
+ if (fkStatus && fkStatus.foreign_keys === 1) {
320
+ db.run("PRAGMA foreign_keys = ON")
321
+ }
348
322
  }
349
323
 
350
324
  /**
@@ -361,13 +335,13 @@ export function checkAndMigrate(
361
335
  const logger = new SqliteLogger(tableName, migrationLog.getBaseLogger())
362
336
 
363
337
  if (!tableExists(db, tableName)) {
364
- return false // No existing table, no migration needed
338
+ return false
365
339
  }
366
340
 
367
341
  const currentSchema = getTableColumns(db, tableName)
368
342
 
369
343
  if (schemasAreDifferent(currentSchema, newColumns, logger)) {
370
- migrateTable(db, tableName, newColumns, options, tableConstraints, migrationLog)
344
+ migrateTable(db, tableName, newColumns, options, tableConstraints, migrationLog, currentSchema)
371
345
  return true
372
346
  }
373
347
 
package/src/types.ts CHANGED
@@ -240,8 +240,6 @@ export interface TableConstraints<T> {
240
240
  export interface MigrationOptions {
241
241
  /** Whether to preserve data during migration (default: true) */
242
242
  preserveData?: boolean
243
- /** Whether to drop columns not in new schema (default: true) */
244
- dropMissingColumns?: boolean
245
243
  /** How to handle constraint violations during data copy */
246
244
  onConflict?: "fail" | "ignore" | "replace"
247
245
  /** Suffix for temporary table during migration (default: '_migration_temp') */
@@ -630,3 +628,35 @@ export type SqlParameter = SQLQueryBindings
630
628
  * Database row with unknown structure
631
629
  */
632
630
  export type DatabaseRowData = Record<string, SQLQueryBindings>
631
+
632
+ export interface TableColumn {
633
+ cid: number
634
+ name: string
635
+ type: string
636
+ notnull: number
637
+ dflt_value: string | number | null
638
+ pk: number
639
+ }
640
+
641
+ export interface IndexInfo {
642
+ name: string
643
+ sql: string | null
644
+ unique: boolean
645
+ origin: string
646
+ partial: number
647
+ }
648
+
649
+ export interface ForeignKeyInfo {
650
+ id: number
651
+ seq: number
652
+ table: string
653
+ from: string
654
+ to: string
655
+ on_update: string
656
+ on_delete: string
657
+ match: string
658
+ }
659
+
660
+ export interface ForeignKeyStatus {
661
+ foreign_keys: number
662
+ }