bunsql-native-migrate 0.3.1 → 0.4.0

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.
@@ -13,32 +13,32 @@ interface LogOptions {
13
13
  error?: unknown;
14
14
  }
15
15
 
16
- /* eslint-disable @typescript-eslint/no-explicit-any */
17
- function formatError(error: any): void {
16
+ function formatError(error: unknown): void {
18
17
  if (error instanceof Error) {
19
18
  console.log(error.message);
20
19
  if (error.stack) console.log(error.stack);
21
20
  return;
22
21
  }
23
- if (typeof error === "object" && error !== null) {
24
- if ("code" in error && "detail" in error) {
25
- console.table(error);
26
- return;
27
- }
28
- if ("code" in error && "errno" in error) {
29
- console.log(error.code);
30
- console.log(error.errno);
31
- if ("byteOffset" in error) console.log(error.byteOffset);
32
- return;
33
- }
34
- if ("message" in error) {
35
- console.log(error.message);
36
- return;
37
- }
22
+ if (typeof error !== "object" || error === null) {
23
+ console.log(String(error));
24
+ return;
25
+ }
26
+ if ("code" in error && "detail" in error) {
27
+ console.table(error);
28
+ return;
29
+ }
30
+ if ("code" in error && "errno" in error) {
31
+ console.log(error.code);
32
+ console.log(error.errno);
33
+ if ("byteOffset" in error) console.log(error.byteOffset);
34
+ return;
35
+ }
36
+ if ("message" in error) {
37
+ console.log(error.message);
38
+ return;
38
39
  }
39
40
  console.log(String(error));
40
41
  }
41
- /* eslint-enable @typescript-eslint/no-explicit-any */
42
42
 
43
43
  export function log({ text, type, error = null }: LogOptions): void {
44
44
  console.log(colors[type], text);
@@ -17,6 +17,7 @@ export interface MigrationDriver {
17
17
  tryLock?(timeoutSeconds: number): Promise<boolean>;
18
18
  releaseLock?(): Promise<void>;
19
19
  close(): Promise<void>;
20
+ client?(): SQL;
20
21
  }
21
22
 
22
23
  export interface DriverTableOptions {
@@ -1,3 +1,19 @@
1
+ export function resolveSecondsOption(
2
+ option: string,
3
+ value: number | undefined,
4
+ fallback: number,
5
+ ): number {
6
+ if (value === undefined) {
7
+ return fallback;
8
+ }
9
+ if (!Number.isInteger(value) || value < 0) {
10
+ throw new Error(
11
+ `Invalid ${option}: ${String(value)} — expected a non-negative integer of seconds`,
12
+ );
13
+ }
14
+ return value;
15
+ }
16
+
1
17
  export function formatDuration(durationMs: number): string {
2
18
  const roundedMs = Math.round(durationMs);
3
19
  if (roundedMs < 1000) {
@@ -1,26 +1,16 @@
1
1
  import { type SQL } from "bun";
2
2
  import type { DriverTableOptions, MigrationDriver } from "../core/driver.js";
3
- import { backtickQuoted, validateIdentifier } from "../core/identifiers.js";
4
- import { createReservedLock, createSqlDriver } from "./shared.js";
3
+ import { backtickQuoted } from "../core/identifiers.js";
4
+ import {
5
+ createReservedLock,
6
+ createSqlDriver,
7
+ resolveTableRef,
8
+ UNIQUE_INDEX_SUFFIX,
9
+ } from "./shared.js";
5
10
 
6
11
  const LOCK_NAME_PREFIX = "bunsql-native-migrate:";
7
12
 
8
13
  const TABLE_NAME_MAX_LENGTH = 47;
9
- const UNIQUE_INDEX_SUFFIX = "_migration_unique";
10
-
11
- function resolveTableRef(options: DriverTableOptions): {
12
- table: string;
13
- index: string;
14
- name: string;
15
- } {
16
- const tableName = options.tableName ?? "migrations";
17
- validateIdentifier("table", tableName, TABLE_NAME_MAX_LENGTH);
18
- return {
19
- table: backtickQuoted(tableName),
20
- index: backtickQuoted(`${tableName}${UNIQUE_INDEX_SUFFIX}`),
21
- name: tableName,
22
- };
23
- }
24
14
 
25
15
  async function checksumColumnExists(db: SQL, tableName: string): Promise<boolean> {
26
16
  const rows = await db`SELECT column_name FROM information_schema.columns
@@ -39,7 +29,10 @@ async function uniqueIndexExists(db: SQL, tableName: string): Promise<boolean> {
39
29
  }
40
30
 
41
31
  export function create(databaseUrl: string, options: DriverTableOptions = {}): MigrationDriver {
42
- const { table, index, name } = resolveTableRef(options);
32
+ const { table, index, name } = resolveTableRef(options, {
33
+ quote: backtickQuoted,
34
+ maxLength: TABLE_NAME_MAX_LENGTH,
35
+ });
43
36
  return createSqlDriver(
44
37
  databaseUrl,
45
38
  {
@@ -1,32 +1,34 @@
1
1
  import { type SQL } from "bun";
2
2
  import type { DriverTableOptions, MigrationDriver } from "../core/driver.js";
3
3
  import { doubleQuoted, validateIdentifier } from "../core/identifiers.js";
4
- import { createReservedLock, createSqlDriver } from "./shared.js";
4
+ import {
5
+ createReservedLock,
6
+ createSqlDriver,
7
+ resolveTableRef,
8
+ UNIQUE_INDEX_SUFFIX,
9
+ type TableRef,
10
+ } from "./shared.js";
5
11
 
6
12
  const LOCK_SCOPE = "bunsql-native-migrate:up";
7
13
 
8
14
  const IDENTIFIER_MAX_LENGTH = 63;
9
- const UNIQUE_INDEX_SUFFIX = "_migration_unique";
10
15
 
11
- interface TableRef {
12
- table: string;
13
- index: string;
14
- name: string;
16
+ interface PostgresTableRef extends TableRef {
15
17
  schemaName?: string;
16
18
  }
17
19
 
18
- function resolveTableRef(options: DriverTableOptions): TableRef {
19
- const tableName = options.tableName ?? "migrations";
20
- validateIdentifier("table", tableName, IDENTIFIER_MAX_LENGTH);
21
- const index = doubleQuoted(`${tableName}${UNIQUE_INDEX_SUFFIX}`);
20
+ function resolvePostgresTableRef(options: DriverTableOptions): PostgresTableRef {
21
+ const base = resolveTableRef(options, {
22
+ quote: doubleQuoted,
23
+ maxLength: IDENTIFIER_MAX_LENGTH,
24
+ });
22
25
  if (options.schema === undefined) {
23
- return { table: doubleQuoted(tableName), index, name: tableName };
26
+ return base;
24
27
  }
25
28
  validateIdentifier("schema", options.schema, IDENTIFIER_MAX_LENGTH);
26
29
  return {
27
- table: `${doubleQuoted(options.schema)}.${doubleQuoted(tableName)}`,
28
- index,
29
- name: tableName,
30
+ ...base,
31
+ table: `${doubleQuoted(options.schema)}.${base.table}`,
30
32
  schemaName: options.schema,
31
33
  };
32
34
  }
@@ -38,7 +40,7 @@ async function advisoryKeyComponents(lock: SQL): Promise<[number, number]> {
38
40
  }
39
41
 
40
42
  export function create(databaseUrl: string, options: DriverTableOptions = {}): MigrationDriver {
41
- const { table, index, name, schemaName } = resolveTableRef(options);
43
+ const { table, index, name, schemaName } = resolvePostgresTableRef(options);
42
44
  return createSqlDriver(
43
45
  databaseUrl,
44
46
  {
@@ -76,11 +78,18 @@ export function create(databaseUrl: string, options: DriverTableOptions = {}): M
76
78
  VALUES (${migration}, ${checksum})
77
79
  ON CONFLICT (migration) DO NOTHING`;
78
80
  },
79
- createLock: (db) =>
80
- createReservedLock(
81
+ createLock: (db) => {
82
+ let cachedKey: [number, number] | null = null;
83
+ const resolveAdvisoryKey = async (lock: SQL): Promise<[number, number]> => {
84
+ if (cachedKey === null) {
85
+ cachedKey = await advisoryKeyComponents(lock);
86
+ }
87
+ return cachedKey;
88
+ };
89
+ return createReservedLock(
81
90
  db,
82
91
  async (lock) => {
83
- const [first, second] = await advisoryKeyComponents(lock);
92
+ const [first, second] = await resolveAdvisoryKey(lock);
84
93
  const rows =
85
94
  (await lock`SELECT pg_try_advisory_lock(${first}, ${second}) AS locked`) as Array<{
86
95
  locked: boolean;
@@ -88,10 +97,11 @@ export function create(databaseUrl: string, options: DriverTableOptions = {}): M
88
97
  return rows[0]?.locked === true;
89
98
  },
90
99
  async (lock) => {
91
- const [first, second] = await advisoryKeyComponents(lock);
100
+ const [first, second] = await resolveAdvisoryKey(lock);
92
101
  await lock`SELECT pg_advisory_unlock(${first}, ${second})`;
93
102
  },
94
- ),
103
+ );
104
+ },
95
105
  },
96
106
  table,
97
107
  );
@@ -1,5 +1,29 @@
1
1
  import { SQL, type ReservedSQL } from "bun";
2
- import type { ExecutedMigration, MigrationDriver } from "../core/driver.js";
2
+ import type { DriverTableOptions, ExecutedMigration, MigrationDriver } from "../core/driver.js";
3
+ import { validateIdentifier } from "../core/identifiers.js";
4
+
5
+ export const UNIQUE_INDEX_SUFFIX = "_migration_unique";
6
+
7
+ export interface TableRef {
8
+ table: string;
9
+ index: string;
10
+ name: string;
11
+ }
12
+
13
+ interface TableRefSpec {
14
+ quote: (identifier: string) => string;
15
+ maxLength: number;
16
+ }
17
+
18
+ export function resolveTableRef(options: DriverTableOptions, spec: TableRefSpec): TableRef {
19
+ const tableName = options.tableName ?? "migrations";
20
+ validateIdentifier("table", tableName, spec.maxLength);
21
+ return {
22
+ table: spec.quote(tableName),
23
+ index: spec.quote(`${tableName}${UNIQUE_INDEX_SUFFIX}`),
24
+ name: tableName,
25
+ };
26
+ }
3
27
 
4
28
  export interface SqlLock {
5
29
  tryLock(timeoutSeconds: number): Promise<boolean>;
@@ -23,7 +47,10 @@ export function createReservedLock(
23
47
  let lockConnection: ReservedSQL | null = null;
24
48
 
25
49
  return {
26
- async tryLock() {
50
+ async tryLock(_timeoutSeconds: number): Promise<boolean> {
51
+ if (lockConnection !== null) {
52
+ return true;
53
+ }
27
54
  const connection = await db.reserve();
28
55
  lockConnection = connection;
29
56
  try {
@@ -66,6 +93,7 @@ export function createSqlDriver(
66
93
 
67
94
  return {
68
95
  install: () => dialect.install(db),
96
+ client: () => db,
69
97
  async listExecuted() {
70
98
  const rows = await db`SELECT migration, checksum FROM ${db.unsafe(table)} ORDER BY id ASC`;
71
99
  return rows.map(
@@ -1,26 +1,14 @@
1
1
  import type { DriverTableOptions, MigrationDriver } from "../core/driver.js";
2
- import { doubleQuoted, validateIdentifier } from "../core/identifiers.js";
3
- import { createSqlDriver } from "./shared.js";
2
+ import { doubleQuoted } from "../core/identifiers.js";
3
+ import { createSqlDriver, resolveTableRef, UNIQUE_INDEX_SUFFIX } from "./shared.js";
4
4
 
5
5
  const TABLE_NAME_MAX_LENGTH = 128;
6
- const UNIQUE_INDEX_SUFFIX = "_migration_unique";
7
-
8
- function resolveTableRef(options: DriverTableOptions): {
9
- table: string;
10
- index: string;
11
- name: string;
12
- } {
13
- const tableName = options.tableName ?? "migrations";
14
- validateIdentifier("table", tableName, TABLE_NAME_MAX_LENGTH);
15
- return {
16
- table: doubleQuoted(tableName),
17
- index: doubleQuoted(`${tableName}${UNIQUE_INDEX_SUFFIX}`),
18
- name: tableName,
19
- };
20
- }
21
6
 
22
7
  export function create(databaseUrl: string, options: DriverTableOptions = {}): MigrationDriver {
23
- const { table, index, name } = resolveTableRef(options);
8
+ const { table, index, name } = resolveTableRef(options, {
9
+ quote: doubleQuoted,
10
+ maxLength: TABLE_NAME_MAX_LENGTH,
11
+ });
24
12
  return createSqlDriver(
25
13
  databaseUrl,
26
14
  {
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  import { InvalidIdentifierError } from "./core/identifiers.js";
8
8
  import { migrateUp } from "./api/up.js";
9
9
  import { migrateDown } from "./api/down.js";
10
+ import { migrateRedo } from "./api/redo.js";
10
11
  import { migrateStatus } from "./api/status.js";
11
12
  import { installMigrations } from "./api/install.js";
12
13
  import { createMigration } from "./api/create.js";
@@ -20,7 +21,10 @@ import {
20
21
  type MigrateStatusResult,
21
22
  type MigrateUpOptions,
22
23
  type MigrateUpResult,
24
+ type RedoOptions,
25
+ type RedoResult,
23
26
  ChecksumDriftError,
27
+ DatabaseWaitTimeoutError,
24
28
  GitStageError,
25
29
  MigrationLockError,
26
30
  MigrationNotFoundError,
@@ -30,11 +34,13 @@ export {
30
34
  createDriver,
31
35
  migrateUp,
32
36
  migrateDown,
37
+ migrateRedo,
33
38
  migrateStatus,
34
39
  installMigrations,
35
40
  createMigration,
36
41
  markMigrationsApplied,
37
42
  ChecksumDriftError,
43
+ DatabaseWaitTimeoutError,
38
44
  GitStageError,
39
45
  InvalidIdentifierError,
40
46
  MigrationLockError,
@@ -49,6 +55,8 @@ export type {
49
55
  MigrateUpResult,
50
56
  MigrateDownOptions,
51
57
  MigrateDownResult,
58
+ RedoOptions,
59
+ RedoResult,
52
60
  MarkOptions,
53
61
  MarkResult,
54
62
  MigrateStatusResult,