@zudojs/database 0.1.0 → 1.1.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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -17
  3. package/dist/cache/cache.memory.d.ts +167 -0
  4. package/dist/cache/cache.memory.js +186 -40
  5. package/dist/cache/index.d.ts +2 -2
  6. package/dist/cache/index.js +2 -2
  7. package/dist/database/database.core.d.ts +91 -0
  8. package/dist/database/database.core.js +21 -4
  9. package/dist/databaseClient/databaseClient.core.d.ts +171 -14
  10. package/dist/databaseClient/databaseClient.core.js +350 -70
  11. package/dist/databaseClient/databaseClient.errors.d.ts +112 -0
  12. package/dist/databaseClient/databaseClient.errors.js +425 -0
  13. package/dist/databaseClient/index.d.ts +2 -1
  14. package/dist/databaseClient/index.js +2 -1
  15. package/dist/databaseConnection/databaseConnection.manager.d.ts +163 -0
  16. package/dist/databaseConnection/databaseConnection.manager.js +138 -76
  17. package/dist/databaseConnection/index.d.ts +1 -1
  18. package/dist/databaseType/databaseType.type.d.ts +224 -0
  19. package/dist/databaseType/databaseType.type.js +0 -35
  20. package/dist/health/health.check.d.ts +74 -0
  21. package/dist/health/health.check.js +78 -47
  22. package/dist/health/index.d.ts +1 -1
  23. package/dist/health/index.js +1 -1
  24. package/dist/index.d.ts +14 -14
  25. package/dist/index.js +11 -11
  26. package/dist/locks/index.d.ts +2 -2
  27. package/dist/locks/index.js +2 -2
  28. package/dist/locks/locks.core.d.ts +132 -0
  29. package/dist/locks/locks.core.js +150 -85
  30. package/dist/migration/index.d.ts +3 -1
  31. package/dist/migration/index.js +2 -0
  32. package/dist/migration/migration.dialect.d.ts +65 -0
  33. package/dist/migration/migration.dialect.js +63 -0
  34. package/dist/migration/migration.helpers.d.ts +65 -0
  35. package/dist/migration/migration.helpers.js +60 -18
  36. package/dist/migration/migration.runner.d.ts +75 -0
  37. package/dist/migration/migration.runner.js +195 -107
  38. package/dist/migration/migration.types.d.ts +85 -0
  39. package/dist/pagination/index.d.ts +2 -1
  40. package/dist/pagination/index.js +2 -1
  41. package/dist/pagination/pagination.core.d.ts +163 -0
  42. package/dist/pagination/pagination.core.js +78 -9
  43. package/dist/pagination/pagination.keyset.d.ts +55 -0
  44. package/dist/pagination/pagination.keyset.js +108 -0
  45. package/dist/queryBuilder/index.d.ts +4 -3
  46. package/dist/queryBuilder/index.js +3 -2
  47. package/dist/queryBuilder/queryBuilder.core.d.ts +178 -0
  48. package/dist/queryBuilder/queryBuilder.core.js +92 -14
  49. package/dist/queryBuilder/queryBuilder.factory.d.ts +6 -0
  50. package/dist/queryBuilder/queryBuilder.factory.js +0 -25
  51. package/dist/queryBuilder/queryBuilder.filter.d.ts +170 -0
  52. package/dist/queryBuilder/queryBuilder.filter.js +191 -30
  53. package/dist/queryBuilder/queryBuilder.prisma.d.ts +60 -0
  54. package/dist/queryBuilder/queryBuilder.prisma.js +233 -0
  55. package/dist/queryBuilder/queryBuilder.type.d.ts +48 -0
  56. package/dist/relations/index.d.ts +1 -1
  57. package/dist/relations/index.js +1 -1
  58. package/dist/relations/relations.definition.d.ts +184 -0
  59. package/dist/relations/relations.definition.js +186 -19
  60. package/dist/repository/index.d.ts +2 -1
  61. package/dist/repository/index.js +1 -0
  62. package/dist/repository/repository.base.d.ts +283 -0
  63. package/dist/repository/repository.base.js +370 -89
  64. package/dist/repository/repository.errors.d.ts +60 -0
  65. package/dist/repository/repository.errors.js +235 -0
  66. package/dist/seed/index.d.ts +2 -2
  67. package/dist/seed/index.js +1 -1
  68. package/dist/seed/seed.runner.d.ts +160 -0
  69. package/dist/seed/seed.runner.js +194 -225
  70. package/dist/transaction/index.d.ts +1 -1
  71. package/dist/transaction/index.js +1 -1
  72. package/dist/transaction/transaction.core.d.ts +117 -0
  73. package/dist/transaction/transaction.core.js +83 -56
  74. package/dist/unitOfWork/unitOfWork.core.d.ts +44 -0
  75. package/dist/unitOfWork/unitOfWork.core.js +9 -20
  76. package/package.json +44 -24
  77. package/dist/cache/cache.memory.d.ts.map +0 -1
  78. package/dist/cache/cache.memory.js.map +0 -1
  79. package/dist/cache/index.d.ts.map +0 -1
  80. package/dist/cache/index.js.map +0 -1
  81. package/dist/database/database.core.d.ts.map +0 -1
  82. package/dist/database/database.core.js.map +0 -1
  83. package/dist/database/index.d.ts.map +0 -1
  84. package/dist/database/index.js.map +0 -1
  85. package/dist/databaseClient/databaseClient.core.d.ts.map +0 -1
  86. package/dist/databaseClient/databaseClient.core.js.map +0 -1
  87. package/dist/databaseClient/databaseClient.logger.d.ts.map +0 -1
  88. package/dist/databaseClient/databaseClient.logger.js.map +0 -1
  89. package/dist/databaseClient/index.d.ts.map +0 -1
  90. package/dist/databaseClient/index.js.map +0 -1
  91. package/dist/databaseConnection/databaseConnection.manager.d.ts.map +0 -1
  92. package/dist/databaseConnection/databaseConnection.manager.js.map +0 -1
  93. package/dist/databaseConnection/index.d.ts.map +0 -1
  94. package/dist/databaseConnection/index.js.map +0 -1
  95. package/dist/databaseType/databaseType.type.d.ts.map +0 -1
  96. package/dist/databaseType/databaseType.type.js.map +0 -1
  97. package/dist/databaseType/index.d.ts.map +0 -1
  98. package/dist/databaseType/index.js.map +0 -1
  99. package/dist/health/health.check.d.ts.map +0 -1
  100. package/dist/health/health.check.js.map +0 -1
  101. package/dist/health/index.d.ts.map +0 -1
  102. package/dist/health/index.js.map +0 -1
  103. package/dist/index.d.ts.map +0 -1
  104. package/dist/index.js.map +0 -1
  105. package/dist/locks/index.d.ts.map +0 -1
  106. package/dist/locks/index.js.map +0 -1
  107. package/dist/locks/locks.core.d.ts.map +0 -1
  108. package/dist/locks/locks.core.js.map +0 -1
  109. package/dist/migration/index.d.ts.map +0 -1
  110. package/dist/migration/index.js.map +0 -1
  111. package/dist/migration/migration.helpers.d.ts.map +0 -1
  112. package/dist/migration/migration.helpers.js.map +0 -1
  113. package/dist/migration/migration.runner.d.ts.map +0 -1
  114. package/dist/migration/migration.runner.js.map +0 -1
  115. package/dist/migration/migration.types.d.ts.map +0 -1
  116. package/dist/migration/migration.types.js.map +0 -1
  117. package/dist/pagination/index.d.ts.map +0 -1
  118. package/dist/pagination/index.js.map +0 -1
  119. package/dist/pagination/pagination.core.d.ts.map +0 -1
  120. package/dist/pagination/pagination.core.js.map +0 -1
  121. package/dist/queryBuilder/index.d.ts.map +0 -1
  122. package/dist/queryBuilder/index.js.map +0 -1
  123. package/dist/queryBuilder/queryBuilder.core.d.ts.map +0 -1
  124. package/dist/queryBuilder/queryBuilder.core.js.map +0 -1
  125. package/dist/queryBuilder/queryBuilder.factory.d.ts.map +0 -1
  126. package/dist/queryBuilder/queryBuilder.factory.js.map +0 -1
  127. package/dist/queryBuilder/queryBuilder.filter.d.ts.map +0 -1
  128. package/dist/queryBuilder/queryBuilder.filter.js.map +0 -1
  129. package/dist/queryBuilder/queryBuilder.type.d.ts.map +0 -1
  130. package/dist/queryBuilder/queryBuilder.type.js.map +0 -1
  131. package/dist/relations/index.d.ts.map +0 -1
  132. package/dist/relations/index.js.map +0 -1
  133. package/dist/relations/relations.definition.d.ts.map +0 -1
  134. package/dist/relations/relations.definition.js.map +0 -1
  135. package/dist/repository/index.d.ts.map +0 -1
  136. package/dist/repository/index.js.map +0 -1
  137. package/dist/repository/repository.base.d.ts.map +0 -1
  138. package/dist/repository/repository.base.js.map +0 -1
  139. package/dist/seed/index.d.ts.map +0 -1
  140. package/dist/seed/index.js.map +0 -1
  141. package/dist/seed/seed.runner.d.ts.map +0 -1
  142. package/dist/seed/seed.runner.js.map +0 -1
  143. package/dist/transaction/index.d.ts.map +0 -1
  144. package/dist/transaction/index.js.map +0 -1
  145. package/dist/transaction/transaction.core.d.ts.map +0 -1
  146. package/dist/transaction/transaction.core.js.map +0 -1
  147. package/dist/unitOfWork/index.d.ts.map +0 -1
  148. package/dist/unitOfWork/index.js.map +0 -1
  149. package/dist/unitOfWork/unitOfWork.core.d.ts.map +0 -1
  150. package/dist/unitOfWork/unitOfWork.core.js.map +0 -1
@@ -1,5 +1,7 @@
1
- import { DatabaseError } from "@zudojs/errors";
2
- import { Prisma } from "@prisma/client";
1
+ import { DatabaseError, DatabaseOperation, } from "@zudojs/errors";
2
+ import { isDatabaseErrorLike } from "../databaseClient/databaseClient.errors.js";
3
+ import { getSqlDialect } from "../migration/migration.dialect.js";
4
+ import { hashLockKey, validateIdentifier, validateLockKey, } from "../migration/migration.helpers.js";
3
5
  /**
4
6
  * Default seed tracking table.
5
7
  */
@@ -10,22 +12,31 @@ export const DEFAULT_SEED_TABLE = "_seeds";
10
12
  export const DEFAULT_SEED_LOCK = "database:seeds";
11
13
  /**
12
14
  * Runs and tracks database seeds.
15
+ *
16
+ * Every entry point re-reads the applied history *inside* the advisory
17
+ * lock before deciding what to execute, so two runners started together
18
+ * never apply or revert the same seed twice.
13
19
  */
14
20
  export class SeedRunner {
15
21
  client;
16
22
  seeds;
17
23
  tableName;
18
24
  lockKey;
25
+ dialect;
26
+ transactionOptions;
27
+ perItemTransaction;
19
28
  constructor(client, seeds, options = {}) {
20
- if (!client) {
29
+ if (!client)
21
30
  throw new TypeError("A database client is required.");
22
- }
23
31
  this.client = client;
24
32
  this.seeds = normalizeSeeds(seeds);
25
33
  this.tableName = options.tableName ?? DEFAULT_SEED_TABLE;
26
34
  this.lockKey = options.lockKey ?? DEFAULT_SEED_LOCK;
35
+ this.dialect = getSqlDialect(options.dialect);
36
+ this.transactionOptions = { ...options.transaction };
37
+ this.perItemTransaction = options.perItemTransaction ?? true;
27
38
  validateIdentifier(this.tableName, "seed table name");
28
- validateLockKey(this.lockKey);
39
+ validateLockKey(this.lockKey, "seed lock key");
29
40
  }
30
41
  /**
31
42
  * Returns the seed runner status.
@@ -33,276 +44,275 @@ export class SeedRunner {
33
44
  async status() {
34
45
  await this.ensureSeedTable();
35
46
  const applied = await this.getAppliedSeeds();
36
- const appliedNames = new Set(applied.map((seed) => seed.name));
37
- const pending = this.seeds.filter((seed) => !appliedNames.has(seed.name));
38
- return {
39
- pending,
40
- applied,
41
- };
47
+ return { pending: this.computePending(applied), applied };
42
48
  }
43
49
  /**
44
- * Executes every pending seed.
50
+ * Executes every pending seed in order.
45
51
  */
46
52
  async run() {
47
53
  await this.ensureSeedTable();
48
- const applied = await this.getAppliedSeeds();
49
- const appliedNames = new Set(applied.map((seed) => seed.name));
50
- const pending = this.seeds.filter((seed) => !appliedNames.has(seed.name));
51
- if (pending.length === 0) {
52
- return {
53
- applied: [],
54
- skipped: applied,
55
- };
56
- }
54
+ const before = await this.getAppliedSeeds();
55
+ const pending = this.computePending(before);
56
+ if (pending.length === 0)
57
+ return { applied: [], skipped: before };
57
58
  const newlyApplied = [];
58
- await this.client.transaction(async (transaction) => {
59
- await this.acquireSeedLock(transaction);
60
- for (const seed of pending) {
59
+ const skipped = [...before];
60
+ if (!this.perItemTransaction) {
61
+ await this.client.transaction(async (transaction) => {
62
+ await this.acquireSeedLock(transaction);
63
+ const applied = await this.getAppliedSeeds(transaction);
64
+ skipped.push(...applied.filter((r) => !hasName(before, r.name)));
65
+ for (const seed of this.computePending(applied)) {
66
+ await this.executeSeed(transaction, seed);
67
+ newlyApplied.push(await this.recordSeed(transaction, seed));
68
+ }
69
+ }, this.buildTransactionOptions());
70
+ return { applied: newlyApplied, skipped };
71
+ }
72
+ for (const seed of pending) {
73
+ const outcome = await this.client.transaction(async (transaction) => {
74
+ await this.acquireSeedLock(transaction);
75
+ const applied = await this.getAppliedSeeds(transaction);
76
+ const existing = applied.find((r) => r.name === seed.name);
77
+ if (existing)
78
+ return { record: existing, skipped: true };
61
79
  await this.executeSeed(transaction, seed);
62
80
  const record = await this.recordSeed(transaction, seed);
63
- newlyApplied.push(record);
64
- }
65
- });
66
- return {
67
- applied: newlyApplied,
68
- skipped: applied,
69
- };
81
+ return { record, skipped: false };
82
+ }, this.buildTransactionOptions());
83
+ if (outcome.skipped)
84
+ skipped.push(outcome.record);
85
+ else
86
+ newlyApplied.push(outcome.record);
87
+ }
88
+ return { applied: newlyApplied, skipped };
70
89
  }
71
90
  /**
72
- * Executes one named seed.
91
+ * Executes one named seed. Returns the existing record when already
92
+ * applied.
73
93
  */
74
94
  async runOne(name) {
75
95
  validateSeedName(name);
76
96
  await this.ensureSeedTable();
77
97
  const seed = this.seeds.find((candidate) => candidate.name === name);
78
98
  if (!seed) {
79
- throw new DatabaseError(`Seed "${name}" is not registered.`);
80
- }
81
- const applied = await this.getAppliedSeeds();
82
- const existing = applied.find((record) => record.name === name);
83
- if (existing) {
84
- return existing;
99
+ throw this.seedError(`Seed "${name}" is not registered.`, {
100
+ metadata: { name },
101
+ });
85
102
  }
86
- let result;
87
- await this.client.transaction(async (transaction) => {
103
+ return this.client.transaction(async (transaction) => {
88
104
  await this.acquireSeedLock(transaction);
89
105
  const current = await this.getAppliedSeeds(transaction);
90
106
  const alreadyApplied = current.find((record) => record.name === name);
91
- if (alreadyApplied) {
92
- result = alreadyApplied;
93
- return;
94
- }
107
+ if (alreadyApplied)
108
+ return alreadyApplied;
95
109
  await this.executeSeed(transaction, seed);
96
- result = await this.recordSeed(transaction, seed);
97
- });
98
- if (!result) {
99
- throw new DatabaseError(`Seed "${name}" did not produce an execution record.`);
100
- }
101
- return result;
110
+ return this.recordSeed(transaction, seed);
111
+ }, this.buildTransactionOptions());
102
112
  }
103
113
  /**
104
114
  * Rolls back the most recently applied seed.
105
115
  */
106
116
  async rollback() {
107
- await this.ensureSeedTable();
108
- const applied = await this.getAppliedSeeds();
109
- if (applied.length === 0) {
110
- return null;
111
- }
112
- const latest = applied[applied.length - 1];
113
- if (!latest) {
114
- return null;
115
- }
116
- const seed = this.seeds.find((candidate) => candidate.name === latest.name);
117
- if (!seed) {
118
- throw new DatabaseError(`Seed "${latest.name}" is recorded as applied but is not registered.`);
119
- }
120
- if (!seed.rollback) {
121
- throw new DatabaseError(`Seed "${seed.name}" does not define a rollback operation.`);
122
- }
123
- await this.client.transaction(async (transaction) => {
124
- await this.acquireSeedLock(transaction);
125
- await seed.rollback(transaction);
126
- await this.deleteSeedRecord(transaction, seed.name);
127
- });
128
- return latest;
117
+ const [record] = await this.rollbackSteps(1);
118
+ return record ?? null;
129
119
  }
130
120
  /**
131
121
  * Rolls back every applied seed in reverse execution order.
132
122
  */
133
- async rollbackAll() {
134
- await this.ensureSeedTable();
135
- const applied = await this.getAppliedSeeds();
136
- if (applied.length === 0) {
137
- return [];
138
- }
139
- const rolledBack = [];
140
- await this.client.transaction(async (transaction) => {
141
- await this.acquireSeedLock(transaction);
142
- for (let index = applied.length - 1; index >= 0; index -= 1) {
143
- const record = applied[index];
144
- if (!record) {
145
- continue;
146
- }
147
- const seed = this.seeds.find((candidate) => candidate.name === record.name);
148
- if (!seed) {
149
- throw new DatabaseError(`Seed "${record.name}" is not registered.`);
150
- }
151
- if (!seed.rollback) {
152
- throw new DatabaseError(`Seed "${seed.name}" does not define a rollback operation.`);
153
- }
154
- await seed.rollback(transaction);
155
- await this.deleteSeedRecord(transaction, seed.name);
156
- rolledBack.push(record);
157
- }
158
- });
159
- return rolledBack;
123
+ rollbackAll() {
124
+ return this.rollbackSteps(Number.POSITIVE_INFINITY);
160
125
  }
161
126
  /**
162
- * Creates the seed tracking table if it does not exist.
127
+ * Creates the seed tracking table if it does not exist, adding the
128
+ * `sequence` column to tables created by earlier versions.
163
129
  */
164
130
  async ensureSeedTable() {
165
- const table = quoteIdentifier(this.tableName);
131
+ const q = (id) => this.dialect.quoteIdentifier(id);
132
+ const { varchar255, timestamp } = this.dialect.types;
133
+ const ddl = `CREATE TABLE IF NOT EXISTS ${q(this.tableName)} (` +
134
+ `${q("name")} ${varchar255} PRIMARY KEY, ` +
135
+ `${q("applied_at")} ${timestamp} NOT NULL DEFAULT ${this.dialect.currentTimestamp}, ` +
136
+ `${q("sequence")} BIGSERIAL NOT NULL)`;
137
+ const upgrade = `ALTER TABLE ${q(this.tableName)} ` +
138
+ `ADD COLUMN IF NOT EXISTS ${q("sequence")} BIGSERIAL NOT NULL`;
166
139
  try {
167
- await this.client.executeRaw(Prisma.sql `
168
- CREATE TABLE IF NOT EXISTS ${table} (
169
- "name" VARCHAR(255) PRIMARY KEY,
170
- "applied_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
171
- )
172
- `);
140
+ await this.client.executeRawUnsafe(ddl);
141
+ await this.client.executeRawUnsafe(upgrade);
173
142
  }
174
143
  catch (error) {
175
- throw new DatabaseError("Failed to initialize the seed tracking table.", {
144
+ throw this.seedError("Failed to initialize the seed tracking table.", {
176
145
  cause: error,
177
- metadata: {
178
- tableName: this.tableName,
179
- },
146
+ metadata: { tableName: this.tableName },
180
147
  });
181
148
  }
182
149
  }
183
150
  /**
184
- * Returns all applied seeds.
151
+ * Returns all applied seeds ordered by execution sequence.
185
152
  */
186
153
  async getAppliedSeeds(transaction) {
187
- const table = quoteIdentifier(this.tableName);
154
+ const q = (id) => this.dialect.quoteIdentifier(id);
155
+ const sql = `SELECT ${q("name")}, ${q("applied_at")}, ${q("sequence")} ` +
156
+ `FROM ${q(this.tableName)} ORDER BY ${q("sequence")} ASC`;
188
157
  try {
189
- let rows;
190
- if (transaction) {
191
- rows = await transaction.$queryRawUnsafe(`
192
- SELECT
193
- "name",
194
- "applied_at"
195
- FROM ${table}
196
- ORDER BY "applied_at" ASC, "name" ASC
197
- `);
198
- }
199
- else {
200
- rows = await this.client.queryRaw(Prisma.sql `
201
- SELECT
202
- "name",
203
- "applied_at"
204
- FROM ${table}
205
- ORDER BY "applied_at" ASC, "name" ASC
206
- `);
207
- }
158
+ const rows = await this.executor(transaction).query(sql);
208
159
  return rows.map((row) => ({
209
160
  name: row.name,
210
161
  appliedAt: new Date(row.applied_at),
162
+ sequence: Number(row.sequence),
211
163
  }));
212
164
  }
213
165
  catch (error) {
214
- throw new DatabaseError("Failed to read seed execution history.", {
166
+ throw this.seedError("Failed to read seed execution history.", {
215
167
  cause: error,
216
- metadata: {
217
- tableName: this.tableName,
218
- },
168
+ metadata: { tableName: this.tableName },
219
169
  });
220
170
  }
221
171
  }
222
172
  /**
223
- * Executes a seed.
173
+ * Alias of {@link getAppliedSeeds}.
224
174
  */
175
+ getHistory() {
176
+ return this.getAppliedSeeds();
177
+ }
178
+ async rollbackSteps(limit) {
179
+ await this.ensureSeedTable();
180
+ const rolledBack = [];
181
+ if (!this.perItemTransaction) {
182
+ await this.client.transaction(async (transaction) => {
183
+ await this.acquireSeedLock(transaction);
184
+ const applied = [...(await this.getAppliedSeeds(transaction))];
185
+ while (applied.length > 0 && rolledBack.length < limit) {
186
+ const record = applied.pop();
187
+ await this.revertRecord(transaction, record);
188
+ rolledBack.push(record);
189
+ }
190
+ }, this.buildTransactionOptions());
191
+ return rolledBack;
192
+ }
193
+ while (rolledBack.length < limit) {
194
+ const record = await this.client.transaction(async (transaction) => {
195
+ await this.acquireSeedLock(transaction);
196
+ const applied = await this.getAppliedSeeds(transaction);
197
+ const latest = applied[applied.length - 1];
198
+ if (!latest)
199
+ return null;
200
+ await this.revertRecord(transaction, latest);
201
+ return latest;
202
+ }, this.buildTransactionOptions());
203
+ if (!record)
204
+ break;
205
+ rolledBack.push(record);
206
+ }
207
+ return rolledBack;
208
+ }
209
+ async revertRecord(transaction, record) {
210
+ const seed = this.seeds.find((candidate) => candidate.name === record.name);
211
+ if (!seed) {
212
+ throw this.seedError(`Seed "${record.name}" is recorded as applied but is not registered.`, { metadata: { name: record.name } });
213
+ }
214
+ if (!seed.rollback) {
215
+ throw this.seedError(`Seed "${seed.name}" does not define a rollback operation.`, { metadata: { name: seed.name } });
216
+ }
217
+ try {
218
+ await seed.rollback(transaction);
219
+ }
220
+ catch (error) {
221
+ throw this.seedError(`Rollback of seed "${seed.name}" failed.`, {
222
+ cause: error,
223
+ metadata: { name: seed.name },
224
+ });
225
+ }
226
+ await this.deleteSeedRecord(transaction, seed.name);
227
+ }
228
+ computePending(applied) {
229
+ const names = new Set(applied.map((record) => record.name));
230
+ return this.seeds.filter((seed) => !names.has(seed.name));
231
+ }
232
+ buildTransactionOptions() {
233
+ return { ...this.transactionOptions };
234
+ }
235
+ executor(transaction) {
236
+ if (transaction) {
237
+ return {
238
+ query: (sql, ...values) => transaction.$queryRawUnsafe(sql, ...values),
239
+ execute: (sql, ...values) => transaction.$executeRawUnsafe(sql, ...values),
240
+ };
241
+ }
242
+ return {
243
+ query: (sql, ...values) => this.client.queryRawUnsafe(sql, values),
244
+ execute: (sql, ...values) => this.client.executeRawUnsafe(sql, values),
245
+ };
246
+ }
225
247
  async executeSeed(transaction, seed) {
226
248
  try {
227
249
  await seed.run(transaction);
228
250
  }
229
251
  catch (error) {
230
- throw new DatabaseError(`Seed "${seed.name}" failed.`, {
252
+ throw this.seedError(`Seed "${seed.name}" failed.`, {
231
253
  cause: error,
232
- metadata: {
233
- name: seed.name,
234
- order: seed.order ?? 0,
235
- },
254
+ metadata: { name: seed.name, order: seed.order ?? 0 },
236
255
  });
237
256
  }
238
257
  }
239
- /**
240
- * Records a successfully executed seed.
241
- */
242
258
  async recordSeed(transaction, seed) {
243
- const table = quoteIdentifier(this.tableName);
259
+ const q = (id) => this.dialect.quoteIdentifier(id);
260
+ const sql = `INSERT INTO ${q(this.tableName)} (${q("name")}) ` +
261
+ `VALUES (${this.dialect.placeholder(1)}) ` +
262
+ `RETURNING ${q("name")}, ${q("applied_at")}, ${q("sequence")}`;
244
263
  try {
245
- await transaction.$executeRawUnsafe(`
246
- INSERT INTO ${table}
247
- ("name")
248
- VALUES
249
- ($1)
250
- `, seed.name);
264
+ const rows = await this.executor(transaction).query(sql, seed.name);
265
+ const row = rows[0];
251
266
  return {
252
267
  name: seed.name,
253
- appliedAt: new Date(),
268
+ appliedAt: row ? new Date(row.applied_at) : new Date(),
269
+ sequence: row ? Number(row.sequence) : 0,
254
270
  };
255
271
  }
256
272
  catch (error) {
257
- throw new DatabaseError(`Failed to record seed "${seed.name}".`, {
273
+ throw this.seedError(`Failed to record seed "${seed.name}".`, {
258
274
  cause: error,
259
- metadata: {
260
- name: seed.name,
261
- },
275
+ metadata: { name: seed.name },
262
276
  });
263
277
  }
264
278
  }
265
- /**
266
- * Removes a seed execution record.
267
- */
268
279
  async deleteSeedRecord(transaction, name) {
269
- const table = quoteIdentifier(this.tableName);
280
+ const q = (id) => this.dialect.quoteIdentifier(id);
281
+ const sql = `DELETE FROM ${q(this.tableName)} ` +
282
+ `WHERE ${q("name")} = ${this.dialect.placeholder(1)}`;
270
283
  try {
271
- await transaction.$executeRawUnsafe(`
272
- DELETE FROM ${table}
273
- WHERE "name" = $1
274
- `, name);
284
+ await this.executor(transaction).execute(sql, name);
275
285
  }
276
286
  catch (error) {
277
- throw new DatabaseError(`Failed to remove seed record "${name}".`, {
287
+ throw this.seedError(`Failed to remove seed record "${name}".`, {
278
288
  cause: error,
279
- metadata: {
280
- name,
281
- },
289
+ metadata: { name },
282
290
  });
283
291
  }
284
292
  }
285
- /**
286
- * Acquires the advisory lock used for seed execution.
287
- */
288
293
  async acquireSeedLock(transaction) {
289
294
  try {
290
- const lock = hashLockKey(this.lockKey);
291
- await transaction.$executeRaw `
292
- SELECT pg_advisory_xact_lock(
293
- ${lock}
294
- )
295
- `;
295
+ await this.executor(transaction).execute(this.dialect.advisoryTransactionLock(), hashLockKey(this.lockKey));
296
296
  }
297
297
  catch (error) {
298
- throw new DatabaseError("Failed to acquire the database seed lock.", {
298
+ throw this.seedError("Failed to acquire the database seed lock.", {
299
299
  cause: error,
300
- metadata: {
301
- lockKey: this.lockKey,
302
- },
300
+ metadata: { lockKey: this.lockKey },
303
301
  });
304
302
  }
305
303
  }
304
+ seedError(message, options = {}) {
305
+ if (isDatabaseErrorLike(options.cause) && !options.metadata) {
306
+ return options.cause;
307
+ }
308
+ return new DatabaseError(message, {
309
+ ...options,
310
+ operation: DatabaseOperation.MIGRATION,
311
+ });
312
+ }
313
+ }
314
+ function hasName(records, name) {
315
+ return records.some((record) => record.name === name);
306
316
  }
307
317
  /**
308
318
  * Creates a seed runner.
@@ -311,7 +321,7 @@ export function createSeedRunner(client, seeds, options) {
311
321
  return new SeedRunner(client, seeds, options);
312
322
  }
313
323
  /**
314
- * Validates and sorts seed definitions.
324
+ * Validates and sorts seed definitions (stable sort on `order`).
315
325
  */
316
326
  export function normalizeSeeds(seeds) {
317
327
  if (!Array.isArray(seeds)) {
@@ -320,9 +330,7 @@ export function normalizeSeeds(seeds) {
320
330
  const normalized = seeds
321
331
  .map((seed) => {
322
332
  validateSeed(seed);
323
- return Object.freeze({
324
- ...seed,
325
- });
333
+ return Object.freeze({ ...seed });
326
334
  })
327
335
  .sort((first, second) => (first.order ?? 0) - (second.order ?? 0));
328
336
  const names = new Set();
@@ -352,9 +360,6 @@ export function validateSeed(seed) {
352
360
  throw new TypeError(`Seed "${seed.name}" has an invalid rollback function.`);
353
361
  }
354
362
  }
355
- /**
356
- * Validates a seed name.
357
- */
358
363
  function validateSeedName(name) {
359
364
  if (typeof name !== "string" || name.trim().length === 0) {
360
365
  throw new TypeError("Seed name is required.");
@@ -363,40 +368,4 @@ function validateSeedName(name) {
363
368
  throw new TypeError("Seed name cannot exceed 255 characters.");
364
369
  }
365
370
  }
366
- /**
367
- * Validates an SQL identifier.
368
- */
369
- function validateIdentifier(identifier, name) {
370
- if (typeof identifier !== "string" ||
371
- !/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
372
- throw new TypeError(`Invalid ${name}: "${identifier}".`);
373
- }
374
- }
375
- /**
376
- * Validates an advisory lock key.
377
- */
378
- function validateLockKey(lockKey) {
379
- if (typeof lockKey !== "string" || lockKey.trim().length === 0) {
380
- throw new TypeError("A database seed lock key is required.");
381
- }
382
- }
383
- /**
384
- * Quotes a validated SQL identifier.
385
- */
386
- function quoteIdentifier(identifier) {
387
- validateIdentifier(identifier, "identifier");
388
- return `"${identifier}"`;
389
- }
390
- /**
391
- * Generates a deterministic signed 64-bit advisory lock key.
392
- */
393
- function hashLockKey(value) {
394
- let hash = 1469598103934665603n;
395
- const bytes = new TextEncoder().encode(value);
396
- for (const byte of bytes) {
397
- hash ^= BigInt(byte);
398
- hash = BigInt.asIntN(64, hash * 1099511628211n);
399
- }
400
- return BigInt.asIntN(64, hash);
401
- }
402
371
  //# sourceMappingURL=seed.runner.js.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Managed transaction execution with retry support.
5
5
  */
6
- export { TransactionManager, createTransactionManager, withTransaction, withTransactionRetry, createTransactionContext, createTransactionId, isTransactionActive, isTransactionCommitted, isTransactionFailed, type TransactionStatus, type TransactionContext, type ManagedTransactionOptions, } from "./transaction.core.js";
6
+ export { TransactionManager, createTransactionManager, withTransaction, withTransactionRetry, createTransactionContext, createTransactionId, getTransactionContextFromError, isTransactionActive, isTransactionCommitted, isTransactionFailed, type TransactionStatus, type TransactionContext, type TransactionOutcome, type TransactionRetryOptions, type ManagedTransactionOptions, } from "./transaction.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Managed transaction execution with retry support.
5
5
  */
6
- export { TransactionManager, createTransactionManager, withTransaction, withTransactionRetry, createTransactionContext, createTransactionId, isTransactionActive, isTransactionCommitted, isTransactionFailed, } from "./transaction.core.js";
6
+ export { TransactionManager, createTransactionManager, withTransaction, withTransactionRetry, createTransactionContext, createTransactionId, getTransactionContextFromError, isTransactionActive, isTransactionCommitted, isTransactionFailed, } from "./transaction.core.js";
7
7
  //# sourceMappingURL=index.js.map