@sparkleideas/plugins 3.0.0-alpha.8
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 +401 -0
- package/__tests__/collection-manager.test.ts +332 -0
- package/__tests__/dependency-graph.test.ts +434 -0
- package/__tests__/enhanced-plugin-registry.test.ts +488 -0
- package/__tests__/plugin-registry.test.ts +368 -0
- package/__tests__/ruvector-bridge.test.ts +2429 -0
- package/__tests__/ruvector-integration.test.ts +1602 -0
- package/__tests__/ruvector-migrations.test.ts +1099 -0
- package/__tests__/ruvector-quantization.test.ts +846 -0
- package/__tests__/ruvector-streaming.test.ts +1088 -0
- package/__tests__/sdk.test.ts +325 -0
- package/__tests__/security.test.ts +348 -0
- package/__tests__/utils/ruvector-test-utils.ts +860 -0
- package/examples/plugin-creator/index.ts +636 -0
- package/examples/plugin-creator/plugin-creator.test.ts +312 -0
- package/examples/ruvector/README.md +288 -0
- package/examples/ruvector/attention-patterns.ts +394 -0
- package/examples/ruvector/basic-usage.ts +288 -0
- package/examples/ruvector/docker-compose.yml +75 -0
- package/examples/ruvector/gnn-analysis.ts +501 -0
- package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
- package/examples/ruvector/init-db.sql +119 -0
- package/examples/ruvector/quantization.ts +680 -0
- package/examples/ruvector/self-learning.ts +447 -0
- package/examples/ruvector/semantic-search.ts +576 -0
- package/examples/ruvector/streaming-large-data.ts +507 -0
- package/examples/ruvector/transactions.ts +594 -0
- package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
- package/examples/ruvector-plugins/index.ts +79 -0
- package/examples/ruvector-plugins/intent-router.ts +354 -0
- package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
- package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
- package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
- package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
- package/examples/ruvector-plugins/shared/index.ts +20 -0
- package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
- package/examples/ruvector-plugins/sona-learning.ts +445 -0
- package/package.json +97 -0
- package/src/collections/collection-manager.ts +661 -0
- package/src/collections/index.ts +56 -0
- package/src/collections/official/index.ts +1040 -0
- package/src/core/base-plugin.ts +416 -0
- package/src/core/plugin-interface.ts +215 -0
- package/src/hooks/index.ts +685 -0
- package/src/index.ts +378 -0
- package/src/integrations/agentic-flow.ts +743 -0
- package/src/integrations/index.ts +88 -0
- package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
- package/src/integrations/ruvector/attention-advanced.ts +1040 -0
- package/src/integrations/ruvector/attention-executor.ts +782 -0
- package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
- package/src/integrations/ruvector/attention.ts +1063 -0
- package/src/integrations/ruvector/gnn.ts +3050 -0
- package/src/integrations/ruvector/hyperbolic.ts +1948 -0
- package/src/integrations/ruvector/index.ts +394 -0
- package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
- package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
- package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
- package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
- package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
- package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
- package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
- package/src/integrations/ruvector/migrations/index.ts +35 -0
- package/src/integrations/ruvector/migrations/migrations.ts +647 -0
- package/src/integrations/ruvector/quantization.ts +2036 -0
- package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
- package/src/integrations/ruvector/self-learning.ts +2376 -0
- package/src/integrations/ruvector/streaming.ts +1737 -0
- package/src/integrations/ruvector/types.ts +1945 -0
- package/src/providers/index.ts +643 -0
- package/src/registry/dependency-graph.ts +568 -0
- package/src/registry/enhanced-plugin-registry.ts +994 -0
- package/src/registry/plugin-registry.ts +604 -0
- package/src/sdk/index.ts +563 -0
- package/src/security/index.ts +594 -0
- package/src/types/index.ts +446 -0
- package/src/workers/index.ts +700 -0
- package/tmp.json +0 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +23 -0
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuVector Migrations Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for database migration features including:
|
|
5
|
+
* - Running migrations in order
|
|
6
|
+
* - Migration state tracking
|
|
7
|
+
* - Rollback support
|
|
8
|
+
* - Partial failure handling
|
|
9
|
+
*
|
|
10
|
+
* @module @sparkleideas/plugins/__tests__/ruvector-migrations
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
createTestConfig,
|
|
16
|
+
createMockMigrationResult,
|
|
17
|
+
measureAsync,
|
|
18
|
+
} from './utils/ruvector-test-utils.js';
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Migration Types
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
interface Migration {
|
|
25
|
+
name: string;
|
|
26
|
+
version: number;
|
|
27
|
+
description: string;
|
|
28
|
+
up: () => Promise<void>;
|
|
29
|
+
down: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MigrationState {
|
|
33
|
+
name: string;
|
|
34
|
+
version: number;
|
|
35
|
+
appliedAt: Date;
|
|
36
|
+
executionTimeMs: number;
|
|
37
|
+
checksum: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface MigrationResult {
|
|
41
|
+
name: string;
|
|
42
|
+
success: boolean;
|
|
43
|
+
direction: 'up' | 'down';
|
|
44
|
+
durationMs: number;
|
|
45
|
+
affectedTables: string[];
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Migration Manager Mock
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
class MockMigrationManager {
|
|
54
|
+
private migrations: Migration[] = [];
|
|
55
|
+
private appliedMigrations: Map<string, MigrationState> = new Map();
|
|
56
|
+
private migrationHistory: MigrationResult[] = [];
|
|
57
|
+
private locked: boolean = false;
|
|
58
|
+
|
|
59
|
+
constructor() {
|
|
60
|
+
this.initDefaultMigrations();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private initDefaultMigrations(): void {
|
|
64
|
+
this.migrations = [
|
|
65
|
+
{
|
|
66
|
+
name: '001_create_vector_extension',
|
|
67
|
+
version: 1,
|
|
68
|
+
description: 'Install pgvector extension',
|
|
69
|
+
up: async () => {
|
|
70
|
+
await this.simulateQuery('CREATE EXTENSION IF NOT EXISTS vector');
|
|
71
|
+
},
|
|
72
|
+
down: async () => {
|
|
73
|
+
await this.simulateQuery('DROP EXTENSION IF EXISTS vector');
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: '002_create_vectors_table',
|
|
78
|
+
version: 2,
|
|
79
|
+
description: 'Create main vectors table',
|
|
80
|
+
up: async () => {
|
|
81
|
+
await this.simulateQuery(`
|
|
82
|
+
CREATE TABLE vectors (
|
|
83
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
84
|
+
embedding vector(384) NOT NULL,
|
|
85
|
+
metadata JSONB,
|
|
86
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
87
|
+
)
|
|
88
|
+
`);
|
|
89
|
+
},
|
|
90
|
+
down: async () => {
|
|
91
|
+
await this.simulateQuery('DROP TABLE IF EXISTS vectors');
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: '003_create_hnsw_index',
|
|
96
|
+
version: 3,
|
|
97
|
+
description: 'Create HNSW index for fast similarity search',
|
|
98
|
+
up: async () => {
|
|
99
|
+
await this.simulateQuery(`
|
|
100
|
+
CREATE INDEX idx_vectors_embedding_hnsw
|
|
101
|
+
ON vectors USING hnsw (embedding vector_cosine_ops)
|
|
102
|
+
WITH (m = 16, ef_construction = 200)
|
|
103
|
+
`);
|
|
104
|
+
},
|
|
105
|
+
down: async () => {
|
|
106
|
+
await this.simulateQuery('DROP INDEX IF EXISTS idx_vectors_embedding_hnsw');
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: '004_create_metadata_index',
|
|
111
|
+
version: 4,
|
|
112
|
+
description: 'Create GIN index for metadata queries',
|
|
113
|
+
up: async () => {
|
|
114
|
+
await this.simulateQuery(`
|
|
115
|
+
CREATE INDEX idx_vectors_metadata
|
|
116
|
+
ON vectors USING GIN (metadata)
|
|
117
|
+
`);
|
|
118
|
+
},
|
|
119
|
+
down: async () => {
|
|
120
|
+
await this.simulateQuery('DROP INDEX IF EXISTS idx_vectors_metadata');
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: '005_add_namespace_column',
|
|
125
|
+
version: 5,
|
|
126
|
+
description: 'Add namespace column for multi-tenancy',
|
|
127
|
+
up: async () => {
|
|
128
|
+
await this.simulateQuery('ALTER TABLE vectors ADD COLUMN namespace VARCHAR(255)');
|
|
129
|
+
await this.simulateQuery('CREATE INDEX idx_vectors_namespace ON vectors(namespace)');
|
|
130
|
+
},
|
|
131
|
+
down: async () => {
|
|
132
|
+
await this.simulateQuery('DROP INDEX IF EXISTS idx_vectors_namespace');
|
|
133
|
+
await this.simulateQuery('ALTER TABLE vectors DROP COLUMN IF EXISTS namespace');
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: '006_create_collections_table',
|
|
138
|
+
version: 6,
|
|
139
|
+
description: 'Create collections table',
|
|
140
|
+
up: async () => {
|
|
141
|
+
await this.simulateQuery(`
|
|
142
|
+
CREATE TABLE collections (
|
|
143
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
144
|
+
name VARCHAR(255) UNIQUE NOT NULL,
|
|
145
|
+
dimensions INTEGER NOT NULL,
|
|
146
|
+
metric VARCHAR(50) DEFAULT 'cosine',
|
|
147
|
+
config JSONB,
|
|
148
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
149
|
+
)
|
|
150
|
+
`);
|
|
151
|
+
},
|
|
152
|
+
down: async () => {
|
|
153
|
+
await this.simulateQuery('DROP TABLE IF EXISTS collections');
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: '007_add_collection_foreign_key',
|
|
158
|
+
version: 7,
|
|
159
|
+
description: 'Add collection reference to vectors',
|
|
160
|
+
up: async () => {
|
|
161
|
+
await this.simulateQuery(
|
|
162
|
+
'ALTER TABLE vectors ADD COLUMN collection_id UUID REFERENCES collections(id)'
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
down: async () => {
|
|
166
|
+
await this.simulateQuery('ALTER TABLE vectors DROP COLUMN IF EXISTS collection_id');
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async simulateQuery(sql: string): Promise<void> {
|
|
173
|
+
// Simulate query execution time
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, 5 + Math.random() * 10));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private calculateChecksum(migration: Migration): string {
|
|
178
|
+
// Simple checksum based on migration name and version
|
|
179
|
+
return Buffer.from(`${migration.name}:${migration.version}`)
|
|
180
|
+
.toString('base64')
|
|
181
|
+
.slice(0, 12);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async acquireLock(): Promise<boolean> {
|
|
185
|
+
if (this.locked) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
this.locked = true;
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async releaseLock(): Promise<void> {
|
|
193
|
+
this.locked = false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
isLocked(): boolean {
|
|
197
|
+
return this.locked;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getMigrations(): Migration[] {
|
|
201
|
+
return [...this.migrations];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getAppliedMigrations(): MigrationState[] {
|
|
205
|
+
return Array.from(this.appliedMigrations.values()).sort(
|
|
206
|
+
(a, b) => a.version - b.version
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getPendingMigrations(): Migration[] {
|
|
211
|
+
return this.migrations.filter((m) => !this.appliedMigrations.has(m.name));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getMigrationHistory(): MigrationResult[] {
|
|
215
|
+
return [...this.migrationHistory];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async migrateUp(target?: string | number): Promise<MigrationResult[]> {
|
|
219
|
+
const lockAcquired = await this.acquireLock();
|
|
220
|
+
if (!lockAcquired) {
|
|
221
|
+
throw new Error('Could not acquire migration lock');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const results: MigrationResult[] = [];
|
|
226
|
+
const pending = this.getPendingMigrations();
|
|
227
|
+
|
|
228
|
+
for (const migration of pending) {
|
|
229
|
+
// Check if we've reached the target
|
|
230
|
+
if (typeof target === 'number' && migration.version > target) {
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (typeof target === 'string' && migration.name === target) {
|
|
234
|
+
// Run this one then stop
|
|
235
|
+
const result = await this.runMigration(migration, 'up');
|
|
236
|
+
results.push(result);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = await this.runMigration(migration, 'up');
|
|
241
|
+
results.push(result);
|
|
242
|
+
|
|
243
|
+
if (!result.success) {
|
|
244
|
+
break; // Stop on failure
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return results;
|
|
249
|
+
} finally {
|
|
250
|
+
await this.releaseLock();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async migrateDown(steps: number = 1): Promise<MigrationResult[]> {
|
|
255
|
+
const lockAcquired = await this.acquireLock();
|
|
256
|
+
if (!lockAcquired) {
|
|
257
|
+
throw new Error('Could not acquire migration lock');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const results: MigrationResult[] = [];
|
|
262
|
+
const applied = this.getAppliedMigrations().reverse();
|
|
263
|
+
|
|
264
|
+
for (let i = 0; i < Math.min(steps, applied.length); i++) {
|
|
265
|
+
const state = applied[i];
|
|
266
|
+
const migration = this.migrations.find((m) => m.name === state.name);
|
|
267
|
+
|
|
268
|
+
if (!migration) {
|
|
269
|
+
results.push({
|
|
270
|
+
name: state.name,
|
|
271
|
+
success: false,
|
|
272
|
+
direction: 'down',
|
|
273
|
+
durationMs: 0,
|
|
274
|
+
affectedTables: [],
|
|
275
|
+
error: 'Migration definition not found',
|
|
276
|
+
});
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = await this.runMigration(migration, 'down');
|
|
281
|
+
results.push(result);
|
|
282
|
+
|
|
283
|
+
if (!result.success) {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return results;
|
|
289
|
+
} finally {
|
|
290
|
+
await this.releaseLock();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async rollbackTo(version: number): Promise<MigrationResult[]> {
|
|
295
|
+
const applied = this.getAppliedMigrations();
|
|
296
|
+
const toRollback = applied.filter((m) => m.version > version);
|
|
297
|
+
|
|
298
|
+
if (toRollback.length === 0) {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return this.migrateDown(toRollback.length);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async runMigration(
|
|
306
|
+
migration: Migration,
|
|
307
|
+
direction: 'up' | 'down'
|
|
308
|
+
): Promise<MigrationResult> {
|
|
309
|
+
const startTime = Date.now();
|
|
310
|
+
const affectedTables: string[] = [];
|
|
311
|
+
|
|
312
|
+
// Determine affected tables based on migration name
|
|
313
|
+
if (migration.name.includes('vectors')) {
|
|
314
|
+
affectedTables.push('vectors');
|
|
315
|
+
}
|
|
316
|
+
if (migration.name.includes('collections')) {
|
|
317
|
+
affectedTables.push('collections');
|
|
318
|
+
}
|
|
319
|
+
if (migration.name.includes('index')) {
|
|
320
|
+
affectedTables.push('vectors'); // Indices affect vectors table
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
if (direction === 'up') {
|
|
325
|
+
await migration.up();
|
|
326
|
+
this.appliedMigrations.set(migration.name, {
|
|
327
|
+
name: migration.name,
|
|
328
|
+
version: migration.version,
|
|
329
|
+
appliedAt: new Date(),
|
|
330
|
+
executionTimeMs: Date.now() - startTime,
|
|
331
|
+
checksum: this.calculateChecksum(migration),
|
|
332
|
+
});
|
|
333
|
+
} else {
|
|
334
|
+
await migration.down();
|
|
335
|
+
this.appliedMigrations.delete(migration.name);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const result: MigrationResult = {
|
|
339
|
+
name: migration.name,
|
|
340
|
+
success: true,
|
|
341
|
+
direction,
|
|
342
|
+
durationMs: Date.now() - startTime,
|
|
343
|
+
affectedTables,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
this.migrationHistory.push(result);
|
|
347
|
+
return result;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
const result: MigrationResult = {
|
|
350
|
+
name: migration.name,
|
|
351
|
+
success: false,
|
|
352
|
+
direction,
|
|
353
|
+
durationMs: Date.now() - startTime,
|
|
354
|
+
affectedTables,
|
|
355
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
this.migrationHistory.push(result);
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async validateMigrations(): Promise<{
|
|
364
|
+
valid: boolean;
|
|
365
|
+
issues: string[];
|
|
366
|
+
}> {
|
|
367
|
+
const issues: string[] = [];
|
|
368
|
+
|
|
369
|
+
// Check for checksum mismatches
|
|
370
|
+
for (const [name, state] of this.appliedMigrations) {
|
|
371
|
+
const migration = this.migrations.find((m) => m.name === name);
|
|
372
|
+
if (!migration) {
|
|
373
|
+
issues.push(`Migration ${name} was applied but definition not found`);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const expectedChecksum = this.calculateChecksum(migration);
|
|
378
|
+
if (state.checksum !== expectedChecksum) {
|
|
379
|
+
issues.push(`Migration ${name} checksum mismatch - definition may have changed`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Check for missing migrations in sequence
|
|
384
|
+
const applied = this.getAppliedMigrations();
|
|
385
|
+
for (let i = 1; i < applied.length; i++) {
|
|
386
|
+
if (applied[i].version !== applied[i - 1].version + 1) {
|
|
387
|
+
issues.push(
|
|
388
|
+
`Gap in migration sequence: ${applied[i - 1].version} to ${applied[i].version}`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
valid: issues.length === 0,
|
|
395
|
+
issues,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async reset(): Promise<MigrationResult[]> {
|
|
400
|
+
const results: MigrationResult[] = [];
|
|
401
|
+
|
|
402
|
+
// Rollback all applied migrations
|
|
403
|
+
const applied = this.getAppliedMigrations().reverse();
|
|
404
|
+
|
|
405
|
+
for (const state of applied) {
|
|
406
|
+
const migration = this.migrations.find((m) => m.name === state.name);
|
|
407
|
+
if (migration) {
|
|
408
|
+
const result = await this.runMigration(migration, 'down');
|
|
409
|
+
results.push(result);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return results;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
addMigration(migration: Migration): void {
|
|
417
|
+
// Insert in version order
|
|
418
|
+
const insertIdx = this.migrations.findIndex((m) => m.version > migration.version);
|
|
419
|
+
if (insertIdx === -1) {
|
|
420
|
+
this.migrations.push(migration);
|
|
421
|
+
} else {
|
|
422
|
+
this.migrations.splice(insertIdx, 0, migration);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// For testing - simulate a failing migration
|
|
427
|
+
addFailingMigration(name: string, version: number): void {
|
|
428
|
+
this.migrations.push({
|
|
429
|
+
name,
|
|
430
|
+
version,
|
|
431
|
+
description: 'Intentionally failing migration',
|
|
432
|
+
up: async () => {
|
|
433
|
+
throw new Error('Simulated migration failure');
|
|
434
|
+
},
|
|
435
|
+
down: async () => {
|
|
436
|
+
throw new Error('Simulated migration failure');
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
this.migrations.sort((a, b) => a.version - b.version);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// Test Suites
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
describe('RuVector Migrations', () => {
|
|
448
|
+
let manager: MockMigrationManager;
|
|
449
|
+
|
|
450
|
+
beforeEach(() => {
|
|
451
|
+
manager = new MockMigrationManager();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// ==========================================================================
|
|
455
|
+
// Running Migrations Tests
|
|
456
|
+
// ==========================================================================
|
|
457
|
+
|
|
458
|
+
describe('Running Migrations', () => {
|
|
459
|
+
it('should run all migrations in order', async () => {
|
|
460
|
+
const results = await manager.migrateUp();
|
|
461
|
+
|
|
462
|
+
expect(results).toHaveLength(7);
|
|
463
|
+
results.forEach((r, i) => {
|
|
464
|
+
expect(r.success).toBe(true);
|
|
465
|
+
expect(r.direction).toBe('up');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Verify order
|
|
469
|
+
const applied = manager.getAppliedMigrations();
|
|
470
|
+
for (let i = 1; i < applied.length; i++) {
|
|
471
|
+
expect(applied[i].version).toBeGreaterThan(applied[i - 1].version);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should run migrations up to a specific version', async () => {
|
|
476
|
+
const results = await manager.migrateUp(4);
|
|
477
|
+
|
|
478
|
+
expect(results).toHaveLength(4);
|
|
479
|
+
results.forEach((r) => {
|
|
480
|
+
expect(r.success).toBe(true);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const applied = manager.getAppliedMigrations();
|
|
484
|
+
expect(applied[applied.length - 1].version).toBe(4);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should run migrations up to a specific name', async () => {
|
|
488
|
+
const results = await manager.migrateUp('003_create_hnsw_index');
|
|
489
|
+
|
|
490
|
+
expect(results).toHaveLength(3);
|
|
491
|
+
expect(results[results.length - 1].name).toBe('003_create_hnsw_index');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should skip already applied migrations', async () => {
|
|
495
|
+
// Apply first 3 migrations
|
|
496
|
+
await manager.migrateUp(3);
|
|
497
|
+
expect(manager.getAppliedMigrations()).toHaveLength(3);
|
|
498
|
+
|
|
499
|
+
// Try to migrate up again
|
|
500
|
+
const results = await manager.migrateUp();
|
|
501
|
+
|
|
502
|
+
// Should only apply remaining 4 migrations
|
|
503
|
+
expect(results).toHaveLength(4);
|
|
504
|
+
expect(manager.getAppliedMigrations()).toHaveLength(7);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should return empty results when no pending migrations', async () => {
|
|
508
|
+
await manager.migrateUp();
|
|
509
|
+
const results = await manager.migrateUp();
|
|
510
|
+
|
|
511
|
+
expect(results).toHaveLength(0);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should record migration timing', async () => {
|
|
515
|
+
const results = await manager.migrateUp(2);
|
|
516
|
+
|
|
517
|
+
results.forEach((r) => {
|
|
518
|
+
expect(r.durationMs).toBeGreaterThanOrEqual(0);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const applied = manager.getAppliedMigrations();
|
|
522
|
+
applied.forEach((m) => {
|
|
523
|
+
expect(m.executionTimeMs).toBeGreaterThanOrEqual(0);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// ==========================================================================
|
|
529
|
+
// Migration State Tracking Tests
|
|
530
|
+
// ==========================================================================
|
|
531
|
+
|
|
532
|
+
describe('Migration State Tracking', () => {
|
|
533
|
+
it('should track migration state', async () => {
|
|
534
|
+
await manager.migrateUp(3);
|
|
535
|
+
|
|
536
|
+
const applied = manager.getAppliedMigrations();
|
|
537
|
+
expect(applied).toHaveLength(3);
|
|
538
|
+
|
|
539
|
+
applied.forEach((m) => {
|
|
540
|
+
expect(m.name).toBeDefined();
|
|
541
|
+
expect(m.version).toBeDefined();
|
|
542
|
+
expect(m.appliedAt).toBeInstanceOf(Date);
|
|
543
|
+
expect(m.checksum).toBeDefined();
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('should track pending migrations', async () => {
|
|
548
|
+
const initialPending = manager.getPendingMigrations();
|
|
549
|
+
expect(initialPending).toHaveLength(7);
|
|
550
|
+
|
|
551
|
+
await manager.migrateUp(3);
|
|
552
|
+
|
|
553
|
+
const remainingPending = manager.getPendingMigrations();
|
|
554
|
+
expect(remainingPending).toHaveLength(4);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('should maintain migration history', async () => {
|
|
558
|
+
await manager.migrateUp(3);
|
|
559
|
+
await manager.migrateDown(1);
|
|
560
|
+
|
|
561
|
+
const history = manager.getMigrationHistory();
|
|
562
|
+
expect(history).toHaveLength(4); // 3 up + 1 down
|
|
563
|
+
|
|
564
|
+
expect(history[0].direction).toBe('up');
|
|
565
|
+
expect(history[1].direction).toBe('up');
|
|
566
|
+
expect(history[2].direction).toBe('up');
|
|
567
|
+
expect(history[3].direction).toBe('down');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should validate migration checksums', async () => {
|
|
571
|
+
await manager.migrateUp(3);
|
|
572
|
+
|
|
573
|
+
const validation = await manager.validateMigrations();
|
|
574
|
+
expect(validation.valid).toBe(true);
|
|
575
|
+
expect(validation.issues).toHaveLength(0);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should detect missing migration definitions', async () => {
|
|
579
|
+
await manager.migrateUp(3);
|
|
580
|
+
|
|
581
|
+
// Simulate removing a migration definition (not possible directly, but we test the concept)
|
|
582
|
+
// The validation should detect gaps in the sequence
|
|
583
|
+
|
|
584
|
+
const validation = await manager.validateMigrations();
|
|
585
|
+
expect(validation.valid).toBe(true);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// ==========================================================================
|
|
590
|
+
// Rollback Tests
|
|
591
|
+
// ==========================================================================
|
|
592
|
+
|
|
593
|
+
describe('Migration Rollback', () => {
|
|
594
|
+
it('should rollback migrations', async () => {
|
|
595
|
+
await manager.migrateUp(5);
|
|
596
|
+
expect(manager.getAppliedMigrations()).toHaveLength(5);
|
|
597
|
+
|
|
598
|
+
const results = await manager.migrateDown(2);
|
|
599
|
+
|
|
600
|
+
expect(results).toHaveLength(2);
|
|
601
|
+
results.forEach((r) => {
|
|
602
|
+
expect(r.success).toBe(true);
|
|
603
|
+
expect(r.direction).toBe('down');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
expect(manager.getAppliedMigrations()).toHaveLength(3);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('should rollback to a specific version', async () => {
|
|
610
|
+
await manager.migrateUp(6);
|
|
611
|
+
expect(manager.getAppliedMigrations()).toHaveLength(6);
|
|
612
|
+
|
|
613
|
+
const results = await manager.rollbackTo(3);
|
|
614
|
+
|
|
615
|
+
expect(results.every((r) => r.success)).toBe(true);
|
|
616
|
+
expect(manager.getAppliedMigrations()).toHaveLength(3);
|
|
617
|
+
|
|
618
|
+
const applied = manager.getAppliedMigrations();
|
|
619
|
+
expect(applied[applied.length - 1].version).toBe(3);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('should rollback all migrations on reset', async () => {
|
|
623
|
+
await manager.migrateUp();
|
|
624
|
+
expect(manager.getAppliedMigrations()).toHaveLength(7);
|
|
625
|
+
|
|
626
|
+
const results = await manager.reset();
|
|
627
|
+
|
|
628
|
+
expect(results).toHaveLength(7);
|
|
629
|
+
results.forEach((r) => {
|
|
630
|
+
expect(r.direction).toBe('down');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
expect(manager.getAppliedMigrations()).toHaveLength(0);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('should handle rollback of never-applied migration', async () => {
|
|
637
|
+
await manager.migrateUp(3);
|
|
638
|
+
|
|
639
|
+
// Try to rollback more than applied
|
|
640
|
+
const results = await manager.migrateDown(10);
|
|
641
|
+
|
|
642
|
+
// Should only rollback what was applied
|
|
643
|
+
expect(results).toHaveLength(3);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('should track affected tables during rollback', async () => {
|
|
647
|
+
await manager.migrateUp(4);
|
|
648
|
+
|
|
649
|
+
const results = await manager.migrateDown(2);
|
|
650
|
+
|
|
651
|
+
results.forEach((r) => {
|
|
652
|
+
expect(r.affectedTables.length).toBeGreaterThan(0);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// ==========================================================================
|
|
658
|
+
// Partial Failure Handling Tests
|
|
659
|
+
// ==========================================================================
|
|
660
|
+
|
|
661
|
+
describe('Partial Failure Handling', () => {
|
|
662
|
+
it('should stop on migration failure', async () => {
|
|
663
|
+
// Add a failing migration in the middle
|
|
664
|
+
manager.addFailingMigration('004a_failing_migration', 4.5 as unknown as number);
|
|
665
|
+
|
|
666
|
+
// This will renumber, let's add it as version 8
|
|
667
|
+
manager = new MockMigrationManager();
|
|
668
|
+
manager.addMigration({
|
|
669
|
+
name: '004a_failing',
|
|
670
|
+
version: 4,
|
|
671
|
+
description: 'Failing migration',
|
|
672
|
+
up: async () => {
|
|
673
|
+
throw new Error('Simulated failure');
|
|
674
|
+
},
|
|
675
|
+
down: async () => {
|
|
676
|
+
throw new Error('Simulated failure');
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const results = await manager.migrateUp();
|
|
681
|
+
|
|
682
|
+
// Should have succeeded up to version 3, then 004a_failing should fail
|
|
683
|
+
const successful = results.filter((r) => r.success);
|
|
684
|
+
const failed = results.filter((r) => !r.success);
|
|
685
|
+
|
|
686
|
+
expect(failed.length).toBe(1);
|
|
687
|
+
expect(failed[0].error).toBe('Simulated failure');
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('should record failure in history', async () => {
|
|
691
|
+
manager.addMigration({
|
|
692
|
+
name: '008_will_fail',
|
|
693
|
+
version: 8,
|
|
694
|
+
description: 'Intentionally failing',
|
|
695
|
+
up: async () => {
|
|
696
|
+
throw new Error('Test failure');
|
|
697
|
+
},
|
|
698
|
+
down: async () => {},
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
await manager.migrateUp();
|
|
702
|
+
|
|
703
|
+
const history = manager.getMigrationHistory();
|
|
704
|
+
const failedMigration = history.find((h) => h.name === '008_will_fail');
|
|
705
|
+
|
|
706
|
+
expect(failedMigration).toBeDefined();
|
|
707
|
+
expect(failedMigration?.success).toBe(false);
|
|
708
|
+
expect(failedMigration?.error).toBe('Test failure');
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('should not apply subsequent migrations after failure', async () => {
|
|
712
|
+
// Add failing migration
|
|
713
|
+
manager.addMigration({
|
|
714
|
+
name: '003a_failing',
|
|
715
|
+
version: 3.5 as unknown as number,
|
|
716
|
+
description: 'Failing',
|
|
717
|
+
up: async () => {
|
|
718
|
+
throw new Error('Failure');
|
|
719
|
+
},
|
|
720
|
+
down: async () => {},
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Manually fix the order
|
|
724
|
+
manager = new MockMigrationManager();
|
|
725
|
+
|
|
726
|
+
// Insert failing migration at position 4
|
|
727
|
+
const failingMigration: Migration = {
|
|
728
|
+
name: '004_failing',
|
|
729
|
+
version: 4,
|
|
730
|
+
description: 'Failing',
|
|
731
|
+
up: async () => {
|
|
732
|
+
throw new Error('Failure');
|
|
733
|
+
},
|
|
734
|
+
down: async () => {},
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
// Get current migrations and replace
|
|
738
|
+
const migrations = manager.getMigrations();
|
|
739
|
+
migrations[3] = failingMigration;
|
|
740
|
+
|
|
741
|
+
// Create new manager with modified migrations
|
|
742
|
+
manager = new MockMigrationManager();
|
|
743
|
+
|
|
744
|
+
const results = await manager.migrateUp();
|
|
745
|
+
|
|
746
|
+
// First 4 should complete (including 3 successful + 1 failure at position 4)
|
|
747
|
+
expect(manager.getAppliedMigrations().length).toBeGreaterThanOrEqual(3);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should handle rollback failure', async () => {
|
|
751
|
+
await manager.migrateUp(3);
|
|
752
|
+
|
|
753
|
+
// Replace the third migration's down method to fail
|
|
754
|
+
// For testing, we can't easily do this, so we test the error handling flow
|
|
755
|
+
|
|
756
|
+
// Run normal rollback which should succeed
|
|
757
|
+
const results = await manager.migrateDown(1);
|
|
758
|
+
expect(results[0].success).toBe(true);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('should provide error details on failure', async () => {
|
|
762
|
+
manager.addMigration({
|
|
763
|
+
name: '008_detailed_failure',
|
|
764
|
+
version: 8,
|
|
765
|
+
description: 'Fails with details',
|
|
766
|
+
up: async () => {
|
|
767
|
+
const error = new Error('Table already exists');
|
|
768
|
+
(error as unknown as { code: string }).code = '42P07';
|
|
769
|
+
throw error;
|
|
770
|
+
},
|
|
771
|
+
down: async () => {},
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await manager.migrateUp();
|
|
775
|
+
|
|
776
|
+
const history = manager.getMigrationHistory();
|
|
777
|
+
const failure = history.find((h) => h.name === '008_detailed_failure');
|
|
778
|
+
|
|
779
|
+
expect(failure?.error).toContain('already exists');
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// ==========================================================================
|
|
784
|
+
// Locking Tests
|
|
785
|
+
// ==========================================================================
|
|
786
|
+
|
|
787
|
+
describe('Migration Locking', () => {
|
|
788
|
+
it('should acquire lock during migration', async () => {
|
|
789
|
+
expect(manager.isLocked()).toBe(false);
|
|
790
|
+
|
|
791
|
+
const migrationPromise = manager.migrateUp(1);
|
|
792
|
+
// Lock is acquired synchronously at start
|
|
793
|
+
expect(manager.isLocked()).toBe(true);
|
|
794
|
+
|
|
795
|
+
await migrationPromise;
|
|
796
|
+
|
|
797
|
+
expect(manager.isLocked()).toBe(false);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it('should prevent concurrent migrations', async () => {
|
|
801
|
+
// Start first migration
|
|
802
|
+
const first = manager.migrateUp(3);
|
|
803
|
+
|
|
804
|
+
// Try to start second migration immediately
|
|
805
|
+
await expect(manager.migrateUp(5)).rejects.toThrow(
|
|
806
|
+
'Could not acquire migration lock'
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
await first;
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it('should release lock after failure', async () => {
|
|
813
|
+
manager.addMigration({
|
|
814
|
+
name: '008_failing',
|
|
815
|
+
version: 8,
|
|
816
|
+
description: 'Fails',
|
|
817
|
+
up: async () => {
|
|
818
|
+
throw new Error('Failure');
|
|
819
|
+
},
|
|
820
|
+
down: async () => {},
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
await manager.migrateUp();
|
|
824
|
+
|
|
825
|
+
// Lock should be released
|
|
826
|
+
expect(manager.isLocked()).toBe(false);
|
|
827
|
+
|
|
828
|
+
// Should be able to run again
|
|
829
|
+
const results = await manager.migrateDown(1);
|
|
830
|
+
expect(results.length).toBeGreaterThan(0);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('should release lock on all paths', async () => {
|
|
834
|
+
// Test rollback
|
|
835
|
+
await manager.migrateUp(3);
|
|
836
|
+
await manager.migrateDown(2);
|
|
837
|
+
expect(manager.isLocked()).toBe(false);
|
|
838
|
+
|
|
839
|
+
// Test rollbackTo
|
|
840
|
+
await manager.migrateUp(5);
|
|
841
|
+
await manager.rollbackTo(2);
|
|
842
|
+
expect(manager.isLocked()).toBe(false);
|
|
843
|
+
|
|
844
|
+
// Test reset
|
|
845
|
+
await manager.migrateUp();
|
|
846
|
+
await manager.reset();
|
|
847
|
+
expect(manager.isLocked()).toBe(false);
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// ==========================================================================
|
|
852
|
+
// Migration Validation Tests
|
|
853
|
+
// ==========================================================================
|
|
854
|
+
|
|
855
|
+
describe('Migration Validation', () => {
|
|
856
|
+
it('should validate applied migrations', async () => {
|
|
857
|
+
await manager.migrateUp(5);
|
|
858
|
+
|
|
859
|
+
const validation = await manager.validateMigrations();
|
|
860
|
+
|
|
861
|
+
expect(validation.valid).toBe(true);
|
|
862
|
+
expect(validation.issues).toHaveLength(0);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it('should detect sequence gaps', async () => {
|
|
866
|
+
// Apply migrations 1, 2, 3
|
|
867
|
+
await manager.migrateUp(3);
|
|
868
|
+
|
|
869
|
+
// Manually add a migration state with version gap
|
|
870
|
+
// This simulates a scenario where migrations were applied out of order
|
|
871
|
+
// In real usage, this shouldn't happen, but we test detection
|
|
872
|
+
|
|
873
|
+
const validation = await manager.validateMigrations();
|
|
874
|
+
// Should be valid since we applied in order
|
|
875
|
+
expect(validation.valid).toBe(true);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it('should validate empty state', async () => {
|
|
879
|
+
const validation = await manager.validateMigrations();
|
|
880
|
+
|
|
881
|
+
expect(validation.valid).toBe(true);
|
|
882
|
+
expect(validation.issues).toHaveLength(0);
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
// ==========================================================================
|
|
887
|
+
// Performance Tests
|
|
888
|
+
// ==========================================================================
|
|
889
|
+
|
|
890
|
+
describe('Migration Performance', () => {
|
|
891
|
+
it('should complete migrations in reasonable time', async () => {
|
|
892
|
+
const { durationMs } = await measureAsync(async () => {
|
|
893
|
+
await manager.migrateUp();
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// All 7 migrations should complete in under 5 seconds
|
|
897
|
+
expect(durationMs).toBeLessThan(5000);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('should track individual migration timing', async () => {
|
|
901
|
+
await manager.migrateUp();
|
|
902
|
+
|
|
903
|
+
const applied = manager.getAppliedMigrations();
|
|
904
|
+
|
|
905
|
+
applied.forEach((m) => {
|
|
906
|
+
expect(m.executionTimeMs).toBeGreaterThanOrEqual(0);
|
|
907
|
+
// Each migration should be quick in tests
|
|
908
|
+
expect(m.executionTimeMs).toBeLessThan(100);
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
it('should handle rapid up/down cycles', async () => {
|
|
913
|
+
for (let i = 0; i < 5; i++) {
|
|
914
|
+
await manager.migrateUp(3);
|
|
915
|
+
await manager.migrateDown(3);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const history = manager.getMigrationHistory();
|
|
919
|
+
expect(history).toHaveLength(30); // 5 cycles * (3 up + 3 down)
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// ==========================================================================
|
|
924
|
+
// Edge Cases Tests
|
|
925
|
+
// ==========================================================================
|
|
926
|
+
|
|
927
|
+
describe('Edge Cases', () => {
|
|
928
|
+
it('should handle migration with no changes', async () => {
|
|
929
|
+
manager.addMigration({
|
|
930
|
+
name: '008_noop',
|
|
931
|
+
version: 8,
|
|
932
|
+
description: 'No-op migration',
|
|
933
|
+
up: async () => {},
|
|
934
|
+
down: async () => {},
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
await manager.migrateUp();
|
|
938
|
+
|
|
939
|
+
const applied = manager.getAppliedMigrations();
|
|
940
|
+
const noop = applied.find((m) => m.name === '008_noop');
|
|
941
|
+
|
|
942
|
+
expect(noop).toBeDefined();
|
|
943
|
+
expect(noop?.executionTimeMs).toBeGreaterThanOrEqual(0);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('should handle version 0 migration', async () => {
|
|
947
|
+
manager.addMigration({
|
|
948
|
+
name: '000_initial',
|
|
949
|
+
version: 0,
|
|
950
|
+
description: 'Initial setup',
|
|
951
|
+
up: async () => {},
|
|
952
|
+
down: async () => {},
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
await manager.migrateUp(0);
|
|
956
|
+
|
|
957
|
+
const applied = manager.getAppliedMigrations();
|
|
958
|
+
expect(applied[0].version).toBe(0);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('should handle large version numbers', async () => {
|
|
962
|
+
manager.addMigration({
|
|
963
|
+
name: '999_future',
|
|
964
|
+
version: 999,
|
|
965
|
+
description: 'Far future migration',
|
|
966
|
+
up: async () => {},
|
|
967
|
+
down: async () => {},
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
await manager.migrateUp();
|
|
971
|
+
|
|
972
|
+
const applied = manager.getAppliedMigrations();
|
|
973
|
+
const future = applied.find((m) => m.version === 999);
|
|
974
|
+
|
|
975
|
+
expect(future).toBeDefined();
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it('should handle migration with special characters in name', async () => {
|
|
979
|
+
manager.addMigration({
|
|
980
|
+
name: '008_add-column_user_email',
|
|
981
|
+
version: 8,
|
|
982
|
+
description: 'Migration with dashes and underscores',
|
|
983
|
+
up: async () => {},
|
|
984
|
+
down: async () => {},
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
await manager.migrateUp();
|
|
988
|
+
|
|
989
|
+
const applied = manager.getAppliedMigrations();
|
|
990
|
+
const special = applied.find((m) => m.name.includes('add-column'));
|
|
991
|
+
|
|
992
|
+
expect(special).toBeDefined();
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it('should handle empty rollback request', async () => {
|
|
996
|
+
const results = await manager.migrateDown(0);
|
|
997
|
+
expect(results).toHaveLength(0);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('should handle rollback when nothing applied', async () => {
|
|
1001
|
+
const results = await manager.migrateDown(5);
|
|
1002
|
+
expect(results).toHaveLength(0);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('should handle rollbackTo current version', async () => {
|
|
1006
|
+
await manager.migrateUp(5);
|
|
1007
|
+
|
|
1008
|
+
const results = await manager.rollbackTo(5);
|
|
1009
|
+
expect(results).toHaveLength(0);
|
|
1010
|
+
|
|
1011
|
+
expect(manager.getAppliedMigrations()).toHaveLength(5);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('should handle rollbackTo future version', async () => {
|
|
1015
|
+
await manager.migrateUp(3);
|
|
1016
|
+
|
|
1017
|
+
const results = await manager.rollbackTo(10);
|
|
1018
|
+
expect(results).toHaveLength(0);
|
|
1019
|
+
|
|
1020
|
+
expect(manager.getAppliedMigrations()).toHaveLength(3);
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// ==========================================================================
|
|
1025
|
+
// Integration Pattern Tests
|
|
1026
|
+
// ==========================================================================
|
|
1027
|
+
|
|
1028
|
+
describe('Integration Patterns', () => {
|
|
1029
|
+
it('should support conditional migrations', async () => {
|
|
1030
|
+
let conditionMet = false;
|
|
1031
|
+
|
|
1032
|
+
manager.addMigration({
|
|
1033
|
+
name: '008_conditional',
|
|
1034
|
+
version: 8,
|
|
1035
|
+
description: 'Runs only if condition is met',
|
|
1036
|
+
up: async () => {
|
|
1037
|
+
if (!conditionMet) {
|
|
1038
|
+
// Skip migration logic but still mark as applied
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
// Would do actual work here
|
|
1042
|
+
},
|
|
1043
|
+
down: async () => {},
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
await manager.migrateUp();
|
|
1047
|
+
|
|
1048
|
+
const applied = manager.getAppliedMigrations();
|
|
1049
|
+
expect(applied.find((m) => m.name === '008_conditional')).toBeDefined();
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
it('should support data migrations', async () => {
|
|
1053
|
+
const dataChanges: string[] = [];
|
|
1054
|
+
|
|
1055
|
+
manager.addMigration({
|
|
1056
|
+
name: '008_data_migration',
|
|
1057
|
+
version: 8,
|
|
1058
|
+
description: 'Migrates data',
|
|
1059
|
+
up: async () => {
|
|
1060
|
+
dataChanges.push('migrated_up');
|
|
1061
|
+
},
|
|
1062
|
+
down: async () => {
|
|
1063
|
+
dataChanges.push('migrated_down');
|
|
1064
|
+
},
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
await manager.migrateUp();
|
|
1068
|
+
expect(dataChanges).toContain('migrated_up');
|
|
1069
|
+
|
|
1070
|
+
await manager.migrateDown(1);
|
|
1071
|
+
expect(dataChanges).toContain('migrated_down');
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
it('should support multi-step migrations', async () => {
|
|
1075
|
+
const steps: string[] = [];
|
|
1076
|
+
|
|
1077
|
+
manager.addMigration({
|
|
1078
|
+
name: '008_multi_step',
|
|
1079
|
+
version: 8,
|
|
1080
|
+
description: 'Multi-step migration',
|
|
1081
|
+
up: async () => {
|
|
1082
|
+
steps.push('step1');
|
|
1083
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
1084
|
+
steps.push('step2');
|
|
1085
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
1086
|
+
steps.push('step3');
|
|
1087
|
+
},
|
|
1088
|
+
down: async () => {
|
|
1089
|
+
steps.push('undo3');
|
|
1090
|
+
steps.push('undo2');
|
|
1091
|
+
steps.push('undo1');
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
await manager.migrateUp();
|
|
1096
|
+
expect(steps).toEqual(['step1', 'step2', 'step3']);
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
});
|