@southwind-ai/database 0.1.2 → 0.1.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/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./src/dialects.js";
2
+ export * from "./src/postgres-module-migrations.js";
2
3
  export * from "./src/runner.js";
3
4
  export * from "./src/schema/index.js";
4
5
  export * from "./src/sql-store.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/database",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,5 +23,8 @@
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"
26
+ },
27
+ "devDependencies": {
28
+ "pg": "^8.22.0"
26
29
  }
27
30
  }
@@ -0,0 +1,69 @@
1
+ import type { MigrationBundle } from "@southwind-ai/shared";
2
+ import { PlatformMigrationRunner, type MigrationRunReport } from "./runner.js";
3
+ import {
4
+ createPostgresStatementExecutor,
5
+ PostgresMigrationHistoryStore,
6
+ type QueryableClient,
7
+ } from "./sql-store.js";
8
+
9
+ const MODULE_MIGRATION_LOCK = "windy.module-migrations.v1";
10
+
11
+ export interface PostgresTransactionClient extends QueryableClient {
12
+ release(): void;
13
+ }
14
+
15
+ export interface PostgresTransactionPool {
16
+ connect(): Promise<PostgresTransactionClient>;
17
+ }
18
+
19
+ export interface PostgresMigrationBundleOptions {
20
+ pool: PostgresTransactionPool;
21
+ bundles: readonly MigrationBundle[];
22
+ now?: () => Date;
23
+ }
24
+
25
+ export async function runPostgresMigrationBundles(
26
+ options: PostgresMigrationBundleOptions,
27
+ ): Promise<MigrationRunReport> {
28
+ const client = await options.pool.connect();
29
+ let transactionOpen = false;
30
+ try {
31
+ await client.query("begin");
32
+ transactionOpen = true;
33
+ await client.query("select pg_advisory_xact_lock(hashtext($1))", [
34
+ MODULE_MIGRATION_LOCK,
35
+ ]);
36
+ const runner = new PlatformMigrationRunner({
37
+ dialect: "postgresql",
38
+ store: new PostgresMigrationHistoryStore(client),
39
+ execute: createPostgresStatementExecutor(client),
40
+ now: options.now,
41
+ recordFailures: false,
42
+ });
43
+ const report = await runner.runBundles(options.bundles);
44
+ const failure = report.failed[0];
45
+ if (failure) {
46
+ throw new Error(
47
+ `Module Migration ${failure.id} 执行失败:${failure.error}`,
48
+ );
49
+ }
50
+ await client.query("commit");
51
+ transactionOpen = false;
52
+ return report;
53
+ } catch (error) {
54
+ if (transactionOpen) await rollbackWithoutMasking(client);
55
+ throw error;
56
+ } finally {
57
+ client.release();
58
+ }
59
+ }
60
+
61
+ async function rollbackWithoutMasking(
62
+ client: PostgresTransactionClient,
63
+ ): Promise<void> {
64
+ try {
65
+ await client.query("rollback");
66
+ } catch {
67
+ // 原始 migration 错误决定启动失败原因,连接释放后由 Pool 负责处置。
68
+ }
69
+ }
package/src/runner.ts CHANGED
@@ -17,11 +17,14 @@ export interface MigrationRunnerOptions {
17
17
  store: MigrationHistoryStore;
18
18
  execute?: (statement: MigrationStatement) => Promise<void>;
19
19
  now?: () => Date;
20
+ recordFailures?: boolean;
20
21
  }
21
22
 
22
23
  export interface MigrationPlanItem {
23
24
  id: string;
24
25
  module: string;
26
+ moduleVersion: string;
27
+ migrationOrder: number;
25
28
  description: string;
26
29
  direction: "up" | "down";
27
30
  checksum: string;
@@ -62,9 +65,15 @@ export class PlatformMigrationRunner {
62
65
  ): Promise<MigrationPlanItem[]> {
63
66
  const ordered = validateAndOrderBundles(bundles);
64
67
  const applied = appliedById(await this.options.store.listApplied());
68
+ let migrationOrder = 0;
65
69
  return ordered.flatMap((bundle) =>
66
70
  orderBundleMigrations(bundle).map((migration) =>
67
- planItem(migration, applied.get(migration.id)),
71
+ planItem(
72
+ migration,
73
+ bundle.version,
74
+ migrationOrder++,
75
+ applied.get(migration.id),
76
+ ),
68
77
  ),
69
78
  );
70
79
  }
@@ -119,7 +128,7 @@ export class PlatformMigrationRunner {
119
128
  } catch (error) {
120
129
  const message = error instanceof Error ? error.message : String(error);
121
130
  failed.push({ id: item.id, error: message });
122
- if (!dryRun) {
131
+ if (!dryRun && this.options.recordFailures !== false) {
123
132
  await this.recordResult(item, stepStartedAt, stepStatements, message);
124
133
  }
125
134
  break;
@@ -147,6 +156,8 @@ export class PlatformMigrationRunner {
147
156
  await this.options.store.record({
148
157
  id: item.id,
149
158
  module: item.module,
159
+ moduleVersion: item.moduleVersion,
160
+ migrationOrder: item.migrationOrder,
150
161
  direction: item.direction,
151
162
  status: error ? "failed" : "applied",
152
163
  checksum: item.checksum,
@@ -164,6 +175,8 @@ function appliedById(entries: readonly MigrationHistoryEntry[]) {
164
175
 
165
176
  function planItem(
166
177
  migration: BundleMigrationDefinition,
178
+ moduleVersion: string,
179
+ migrationOrder: number,
167
180
  applied: MigrationHistoryEntry | undefined,
168
181
  ): MigrationPlanItem {
169
182
  if (applied?.module !== undefined && applied.module !== migration.module) {
@@ -182,6 +195,8 @@ function planItem(
182
195
  return {
183
196
  id: migration.id,
184
197
  module: migration.module,
198
+ moduleVersion,
199
+ migrationOrder,
185
200
  description: migration.description,
186
201
  direction: migration.direction,
187
202
  checksum: migration.checksum,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  index,
3
+ integer,
3
4
  jsonb,
4
5
  pgTable,
5
6
  text,
@@ -12,6 +13,8 @@ export const windyMigrationHistory = pgTable(
12
13
  {
13
14
  id: varchar("id", { length: 160 }).primaryKey(),
14
15
  module: varchar("module", { length: 96 }).notNull(),
16
+ moduleVersion: varchar("module_version", { length: 64 }),
17
+ migrationOrder: integer("migration_order"),
15
18
  direction: varchar("direction", { length: 16 }).notNull(),
16
19
  status: varchar("status", { length: 16 }).notNull(),
17
20
  checksum: varchar("checksum", { length: 128 }),
package/src/sql-store.ts CHANGED
@@ -15,6 +15,8 @@ export interface QueryableClient {
15
15
  interface MigrationHistoryRow {
16
16
  id: string;
17
17
  module: string;
18
+ module_version: string | null;
19
+ migration_order: number | null;
18
20
  direction: "up" | "down";
19
21
  status: "applied" | "failed";
20
22
  checksum: string | null;
@@ -29,7 +31,8 @@ export class PostgresMigrationHistoryStore implements MigrationHistoryStore {
29
31
 
30
32
  async listApplied(): Promise<MigrationHistoryEntry[]> {
31
33
  const result = await this.client.query<MigrationHistoryRow>(
32
- `select id, module, direction, status, checksum, statements, error, started_at, finished_at
34
+ `select id, module, module_version, migration_order, direction, status,
35
+ checksum, statements, error, started_at, finished_at
33
36
  from windy_migration_history
34
37
  where status = $1
35
38
  order by finished_at asc`,
@@ -42,10 +45,13 @@ export class PostgresMigrationHistoryStore implements MigrationHistoryStore {
42
45
  async record(entry: MigrationHistoryEntry): Promise<void> {
43
46
  await this.client.query(
44
47
  `insert into windy_migration_history
45
- (id, module, direction, status, checksum, statements, error, started_at, finished_at)
46
- values ($1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9)
48
+ (id, module, module_version, migration_order, direction, status,
49
+ checksum, statements, error, started_at, finished_at)
50
+ values ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11)
47
51
  on conflict (id) do update set
48
52
  module = excluded.module,
53
+ module_version = excluded.module_version,
54
+ migration_order = excluded.migration_order,
49
55
  direction = excluded.direction,
50
56
  status = excluded.status,
51
57
  checksum = excluded.checksum,
@@ -56,6 +62,8 @@ export class PostgresMigrationHistoryStore implements MigrationHistoryStore {
56
62
  [
57
63
  entry.id,
58
64
  entry.module,
65
+ entry.moduleVersion || null,
66
+ entry.migrationOrder ?? null,
59
67
  entry.direction,
60
68
  entry.status,
61
69
  entry.checksum || null,
@@ -86,6 +94,8 @@ function mapHistoryRow(row: MigrationHistoryRow): MigrationHistoryEntry {
86
94
  return {
87
95
  id: row.id,
88
96
  module: row.module,
97
+ moduleVersion: row.module_version || undefined,
98
+ migrationOrder: row.migration_order ?? undefined,
89
99
  direction: row.direction,
90
100
  status: row.status,
91
101
  startedAt: toIsoString(row.started_at),
package/src/store.ts CHANGED
@@ -5,6 +5,8 @@ export type MigrationStatus = "applied" | "failed";
5
5
  export interface MigrationHistoryEntry {
6
6
  id: string;
7
7
  module: string;
8
+ moduleVersion?: string;
9
+ migrationOrder?: number;
8
10
  direction: "up" | "down";
9
11
  status: MigrationStatus;
10
12
  startedAt: ISODateTime;