Przejdź do głównej zawartości

Migracje baz danych/kodu

Migracje to kręgosłup ewoluującego oprogramowania. Czy zmieniasz schematy baz danych, uaktualniasz frameworki, czy przekształcasz formaty danych, migracje muszą być precyzyjne, odwracalne i bezpieczne. Claude Code przekształca rozwój migracji z nerwowego procesu w systematyczny, inspirujący zaufanie workflow. Ta lekcja bada jak wykorzystać pomoc AI dla wszystkich typów migracji.

Scenariusz: Twoja aplikacja rozrosła się ze startupu do scale-up. Schemat bazy danych, który działał dla 100 użytkowników, trzeszczy pod 100,000. Framework, który wybrałeś trzy lata temu, ma breaking changes w najnowszej wersji. Format danych, który wydawał się prosty, teraz wymaga całkowitej restrukturyzacji. Tradycyjne podejście: miesiące planowania, krzyżowania palców i nocnych rollbacków. Z Claude Code: systematyczne migracje z pewnością siebie.

Tydzień 1: Planowanie
- Ręczne mapowanie wszystkich zmian
- Nadzieja, że nic nie zostało pominięte
- Stworzenie skryptów migracji
Tydzień 2: Testowanie
- Uruchomienie na stagingu
- Znajdowanie przypadków brzegowych
- Naprawianie i powtarzanie
Tydzień 3: Wdrożenie
- Zaplanowanie przestoju
- Uruchomienie migracji
- Modlitwa żeby działało
Tydzień 4: Gaszenie pożarów
- Obsługa rollbacków
- Naprawa korupcji danych
- Przepraszanie użytkowników
> Przeanalizuj nasz schemat bazy danych i stwórz migrację do:
> - Dodania systemu ról i uprawnień użytkowników
> - Implementacji soft delete w wszystkich tabelach
> - Dodania kolumn logowania audytu
> - Optymalizacji indeksów dla obecnych wzorców zapytań
> Dołącz procedury rollback i zachowanie danych

Claude tworzy kompletną strategię migracji:

  1. Analiza schematu

    schema-analyzer.js
    const { Client } = require('pg');
    class SchemaAnalyzer {
    constructor(connectionString) {
    this.client = new Client({ connectionString });
    }
    async analyze() {
    await this.client.connect();
    const analysis = {
    tables: await this.getTables(),
    indexes: await this.getIndexes(),
    constraints: await this.getConstraints(),
    dependencies: await this.getDependencies(),
    currentQueries: await this.analyzeQueryPatterns()
    };
    await this.client.end();
    return analysis;
    }
    async getTables() {
    const result = await this.client.query(`
    SELECT
    table_name,
    column_name,
    data_type,
    is_nullable,
    column_default
    FROM information_schema.columns
    WHERE table_schema = 'public'
    ORDER BY table_name, ordinal_position
    `);
    // Grupuj według tabeli
    const tables = {};
    for (const row of result.rows) {
    if (!tables[row.table_name]) {
    tables[row.table_name] = {
    name: row.table_name,
    columns: []
    };
    }
    tables[row.table_name].columns.push({
    name: row.column_name,
    type: row.data_type,
    nullable: row.is_nullable === 'YES',
    default: row.column_default
    });
    }
    return tables;
    }
    async getIndexes() {
    const result = await this.client.query(`
    SELECT
    schemaname,
    tablename,
    indexname,
    indexdef
    FROM pg_indexes
    WHERE schemaname = 'public'
    `);
    return result.rows;
    }
    async analyzeQueryPatterns() {
    // Analizuj pg_stat_statements jeśli dostępne
    try {
    const result = await this.client.query(`
    SELECT
    query,
    calls,
    mean_exec_time,
    total_exec_time
    FROM pg_stat_statements
    WHERE query NOT LIKE '%pg_%'
    ORDER BY total_exec_time DESC
    LIMIT 50
    `);
    return result.rows;
    } catch (error) {
    // Rozszerzenie może nie być zainstalowane
    return [];
    }
    }
    }
  2. Generator migracji

    migration-generator.js
    class MigrationGenerator {
    constructor(analysis) {
    this.analysis = analysis;
    this.timestamp = new Date().toISOString().replace(/[^\d]/g, '').slice(0, 14);
    }
    generateRolesMigration() {
    const upQueries = [];
    const downQueries = [];
    // Stwórz tabelę ról
    upQueries.push(`
    CREATE TABLE IF NOT EXISTS roles (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    `);
    downQueries.push('DROP TABLE IF EXISTS roles CASCADE;');
    // Stwórz tabelę uprawnień
    upQueries.push(`
    CREATE TABLE IF NOT EXISTS permissions (
    id SERIAL PRIMARY KEY,
    resource VARCHAR(100) NOT NULL,
    action VARCHAR(50) NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(resource, action)
    );
    `);
    downQueries.push('DROP TABLE IF EXISTS permissions CASCADE;');
    // Stwórz tabelę łączącą role_permissions
    upQueries.push(`
    CREATE TABLE IF NOT EXISTS role_permissions (
    role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
    permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
    granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (role_id, permission_id)
    );
    `);
    downQueries.push('DROP TABLE IF EXISTS role_permissions CASCADE;');
    // Dodaj role_id do tabeli users
    upQueries.push(`
    ALTER TABLE users
    ADD COLUMN role_id INTEGER REFERENCES roles(id);
    `);
    downQueries.push('ALTER TABLE users DROP COLUMN IF EXISTS role_id;');
    // Stwórz domyślne role
    upQueries.push(`
    INSERT INTO roles (name, description) VALUES
    ('admin', 'Pełny dostęp do systemu'),
    ('user', 'Standardowy dostęp użytkownika'),
    ('guest', 'Ograniczony dostęp tylko do odczytu')
    ON CONFLICT (name) DO NOTHING;
    `);
    // Stwórz indeksy
    upQueries.push(`
    CREATE INDEX idx_users_role_id ON users(role_id);
    CREATE INDEX idx_role_permissions_role_id ON role_permissions(role_id);
    CREATE INDEX idx_role_permissions_permission_id ON role_permissions(permission_id);
    `);
    return {
    up: upQueries,
    down: downQueries.reverse()
    };
    }
    generateSoftDeletesMigration() {
    const upQueries = [];
    const downQueries = [];
    // Dodaj kolumny soft delete do wszystkich tabel
    for (const tableName of Object.keys(this.analysis.tables)) {
    const table = this.analysis.tables[tableName];
    // Pomiń tabele systemowe i już soft-deletable tabele
    if (tableName.startsWith('pg_') ||
    table.columns.some(c => c.name === 'deleted_at')) {
    continue;
    }
    upQueries.push(`
    ALTER TABLE ${tableName}
    ADD COLUMN deleted_at TIMESTAMP DEFAULT NULL,
    ADD COLUMN deleted_by INTEGER REFERENCES users(id);
    `);
    downQueries.push(`
    ALTER TABLE ${tableName}
    DROP COLUMN IF EXISTS deleted_at,
    DROP COLUMN IF EXISTS deleted_by;
    `);
    // Stwórz częściowy indeks dla nieusunięte rekordów
    upQueries.push(`
    CREATE INDEX idx_${tableName}_not_deleted
    ON ${tableName}(id)
    WHERE deleted_at IS NULL;
    `);
    downQueries.push(`
    DROP INDEX IF EXISTS idx_${tableName}_not_deleted;
    `);
    // Stwórz widok dla aktywnych rekordów
    upQueries.push(`
    CREATE OR REPLACE VIEW ${tableName}_active AS
    SELECT * FROM ${tableName}
    WHERE deleted_at IS NULL;
    `);
    downQueries.push(`
    DROP VIEW IF EXISTS ${tableName}_active;
    `);
    }
    return {
    up: upQueries,
    down: downQueries.reverse()
    };
    }
    generateAuditMigration() {
    const upQueries = [];
    const downQueries = [];
    // Stwórz tabelę logów audytu
    upQueries.push(`
    CREATE TABLE IF NOT EXISTS audit_logs (
    id BIGSERIAL PRIMARY KEY,
    table_name VARCHAR(100) NOT NULL,
    record_id INTEGER NOT NULL,
    action VARCHAR(20) NOT NULL,
    user_id INTEGER REFERENCES users(id),
    changes JSONB,
    metadata JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    `);
    downQueries.push('DROP TABLE IF EXISTS audit_logs CASCADE;');
    // Stwórz indeksy
    upQueries.push(`
    CREATE INDEX idx_audit_logs_table_record ON audit_logs(table_name, record_id);
    CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
    CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
    CREATE INDEX idx_audit_logs_action ON audit_logs(action);
    `);
    // Dodaj kolumny audytu do wszystkich tabel
    for (const tableName of Object.keys(this.analysis.tables)) {
    if (tableName.startsWith('pg_') || tableName === 'audit_logs') continue;
    const table = this.analysis.tables[tableName];
    // Dodaj created_by jeśli nie istnieje
    if (!table.columns.some(c => c.name === 'created_by')) {
    upQueries.push(`
    ALTER TABLE ${tableName}
    ADD COLUMN created_by INTEGER REFERENCES users(id);
    `);
    downQueries.push(`
    ALTER TABLE ${tableName}
    DROP COLUMN IF EXISTS created_by;
    `);
    }
    // Dodaj updated_by jeśli nie istnieje
    if (!table.columns.some(c => c.name === 'updated_by')) {
    upQueries.push(`
    ALTER TABLE ${tableName}
    ADD COLUMN updated_by INTEGER REFERENCES users(id);
    `);
    downQueries.push(`
    ALTER TABLE ${tableName}
    DROP COLUMN IF EXISTS updated_by;
    `);
    }
    // Dodaj updated_at z triggerem jeśli nie istnieje
    if (!table.columns.some(c => c.name === 'updated_at')) {
    upQueries.push(`
    ALTER TABLE ${tableName}
    ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
    CREATE OR REPLACE FUNCTION update_${tableName}_updated_at()
    RETURNS TRIGGER AS $$
    BEGIN
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    CREATE TRIGGER trigger_update_${tableName}_updated_at
    BEFORE UPDATE ON ${tableName}
    FOR EACH ROW
    EXECUTE FUNCTION update_${tableName}_updated_at();
    `);
    downQueries.push(`
    DROP TRIGGER IF EXISTS trigger_update_${tableName}_updated_at ON ${tableName};
    DROP FUNCTION IF EXISTS update_${tableName}_updated_at();
    ALTER TABLE ${tableName} DROP COLUMN IF EXISTS updated_at;
    `);
    }
    }
    return {
    up: upQueries,
    down: downQueries.reverse()
    };
    }
    generateIndexOptimization() {
    const upQueries = [];
    const downQueries = [];
    // Analizuj wzorce zapytań aby zasugerować nowe indeksy
    const queryPatterns = this.analysis.currentQueries;
    for (const pattern of queryPatterns) {
    // Proste dopasowanie wzorców dla klauzul WHERE
    const whereMatch = pattern.query.match(/WHERE\s+(\w+)\.?(\w+)\s*=/i);
    if (whereMatch) {
    const [_, tableAlias, column] = whereMatch;
    // Sprawdź czy indeks istnieje
    const indexExists = this.analysis.indexes.some(idx =>
    idx.indexdef.includes(column)
    );
    if (!indexExists && pattern.mean_exec_time > 100) {
    // Zasugeruj indeks dla wolnych zapytań
    upQueries.push(`
    -- Zapytanie średnio ${pattern.mean_exec_time}ms
    -- ${pattern.query.substring(0, 100)}...
    CREATE INDEX CONCURRENTLY idx_${tableAlias}_${column}_perf
    ON ${tableAlias}(${column});
    `);
    downQueries.push(`
    DROP INDEX IF EXISTS idx_${tableAlias}_${column}_perf;
    `);
    }
    }
    }
    return {
    up: upQueries,
    down: downQueries.reverse()
    };
    }
    generateCompleteMigration() {
    const migrations = [
    this.generateRolesMigration(),
    this.generateSoftDeletesMigration(),
    this.generateAuditMigration(),
    this.generateIndexOptimization()
    ];
    const combined = {
    up: [],
    down: []
    };
    // Połącz wszystkie migracje
    for (const migration of migrations) {
    combined.up.push(...migration.up);
    combined.down.push(...migration.down);
    }
    return this.wrapInTransaction(combined);
    }
    wrapInTransaction(migration) {
    return {
    filename: `${this.timestamp}_comprehensive_schema_update.sql`,
    up: `

— Migracja: Kompleksowa aktualizacja schematu — Wygenerowano: + new Date().toISOString() + — Opis: Dodaj role, uprawnienia, soft delete, logowanie audytu i optymalizuj indeksy

BEGIN;

+ migration.up.join('\n\n') +

— Zweryfikuj migrację DO $$ BEGIN — Sprawdź czy tabela ról istnieje IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = ‘roles’) THEN RAISE EXCEPTION ‘Migracja nieudana: tabela ról nie została stworzona’; END IF;

— Sprawdź kolumny soft delete IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = ‘users’ AND column_name = ‘deleted_at’ ) THEN RAISE EXCEPTION ‘Migracja nieudana: kolumny soft delete nie zostały dodane’; END IF;

RAISE NOTICE ‘Weryfikacja migracji przeszła’; END $$;

COMMIT; , down: — Rollback: Kompleksowa aktualizacja schematu — Wygenerowano: + new Date().toISOString() +

BEGIN;

+ migration.down.join('\n\n') +

COMMIT; ` }; } }

migration-executor.js
3. **Executor migracji**
```javascript
class MigrationExecutor {
constructor(connectionString) {
this.connectionString = connectionString;
}
async execute(migration, direction = 'up') {
const client = new Client({ connectionString: this.connectionString });
try {
await client.connect();
console.log(`Wykonywanie migracji: ${migration.filename} (${direction})`);
// Rozpocznij pomiar czasu
const startTime = Date.now();
// Wykonaj migrację
await client.query(migration[direction]);
// Zapisz w tabeli migracji
if (direction === 'up') {
await client.query(`
INSERT INTO schema_migrations (version, executed_at)
VALUES ($1, CURRENT_TIMESTAMP)
`, [migration.filename]);
} else {
await client.query(`
DELETE FROM schema_migrations
WHERE version = $1
`, [migration.filename]);
}
const duration = Date.now() - startTime;
console.log(`Migracja zakończona w ${duration}ms`);
return { success: true, duration };
} catch (error) {
console.error(`Migracja nieudana: ${error.message}`);
throw error;
} finally {
await client.end();
}
}
async dryRun(migration) {
console.log('=== TRYB DRY RUN ===');
console.log('Migracja UP:');
console.log(migration.up);
console.log('\nMigracja DOWN:');
console.log(migration.down);
// Waliduj składnię SQL
const client = new Client({ connectionString: this.connectionString });
try {
await client.connect();
// Użyj EXPLAIN do walidacji bez wykonywania
await client.query('BEGIN');
// Parsuj i waliduj każdą instrukcję
const statements = migration.up.split(';').filter(s => s.trim());
for (const statement of statements) {
if (statement.trim() && !statement.trim().startsWith('--')) {
try {
// Dla instrukcji DDL nie możemy użyć EXPLAIN, więc tylko parsuj
await client.query(`DO $$ BEGIN RETURN; END $$;`);
console.log(`✓ Prawidłowe: ${statement.substring(0, 50)}...`);
} catch (error) {
console.error(`✗ Nieprawidłowe: ${statement.substring(0, 50)}...`);
console.error(` Błąd: ${error.message}`);
}
}
}
await client.query('ROLLBACK');
return { valid: true };
} catch (error) {
console.error(`Walidacja nieudana: ${error.message}`);
return { valid: false, error };
} finally {
await client.end();
}
}
}
> Zaimplementuj wzorzec migracji bez przestojów dla:
> - Zmiany nazwy często używanej kolumny
> - Zmiany typów danych kolumn
> - Dzielenia jednej tabeli na wiele tabel
> Dołącz warstwy kompatybilności i stopniowe wdrażanie
zero-downtime-migration.js
class ZeroDowntimeMigration {
constructor(tableName, client) {
this.tableName = tableName;
this.client = client;
}
// Wzorzec 1: Zmiana nazwy kolumny z kompatybilnością wsteczną
async renameColumn(oldName, newName) {
const steps = [];
// Krok 1: Dodaj nową kolumnę jako kolumnę obliczeniową
steps.push({
name: 'Dodaj kolumnę obliczeniową',
up: `
ALTER TABLE ${this.tableName}
ADD COLUMN ${newName} ${await this.getColumnType(oldName)}
GENERATED ALWAYS AS (${oldName}) STORED;
`,
down: `
ALTER TABLE ${this.tableName}
DROP COLUMN ${newName};
`,
validation: async () => {
// Zweryfikuj czy nowa kolumna istnieje i ma te same wartości
const result = await this.client.query(`
SELECT COUNT(*) FROM ${this.tableName}
WHERE ${oldName} IS DISTINCT FROM ${newName}
`);
return result.rows[0].count === '0';
}
});
// Krok 2: Wdróż zmiany aplikacji aby używać nowej kolumny
steps.push({
name: 'Wdróż aplikację używającą nowej kolumny',
manual: true,
instructions: `
1. Zaktualizuj kod aplikacji aby czytać z ${newName}
2. Kontynuuj pisanie do ${oldName}
3. Wdróż i monitoruj problemy
`
});
// Krok 3: Rozpocznij podwójne zapisy
steps.push({
name: 'Stwórz trigger podwójnego zapisu',
up: `
CREATE OR REPLACE FUNCTION sync_${oldName}_to_${newName}()
RETURNS TRIGGER AS $$
BEGIN
NEW.${newName} := NEW.${oldName};
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_sync_${oldName}_to_${newName}
BEFORE INSERT OR UPDATE ON ${this.tableName}
FOR EACH ROW
EXECUTE FUNCTION sync_${oldName}_to_${newName}();
`,
down: `
DROP TRIGGER IF EXISTS trigger_sync_${oldName}_to_${newName} ON ${this.tableName};
DROP FUNCTION IF EXISTS sync_${oldName}_to_${newName}();
`
});
// Krok 4: Backfill wszelkich różnic
steps.push({
name: 'Backfill danych',
up: `
UPDATE ${this.tableName}
SET ${newName} = ${oldName}
WHERE ${newName} IS DISTINCT FROM ${oldName};
`,
validation: async () => {
const result = await this.client.query(`
SELECT COUNT(*) FROM ${this.tableName}
WHERE ${oldName} IS DISTINCT FROM ${newName}
`);
return result.rows[0].count === '0';
}
});
// Krok 5: Przełącz kolumnę główną
steps.push({
name: 'Ustaw nową kolumnę jako główną',
up: `
-- Usuń ograniczenie kolumny generowanej
ALTER TABLE ${this.tableName}
ALTER COLUMN ${newName} DROP EXPRESSION;
-- Usuń starą kolumnę
ALTER TABLE ${this.tableName}
DROP COLUMN ${oldName};
-- Wyczyść trigger
DROP TRIGGER IF EXISTS trigger_sync_${oldName}_to_${newName} ON ${this.tableName};
DROP FUNCTION IF EXISTS sync_${oldName}_to_${newName}();
`,
down: `
-- Dodaj ponownie starą kolumnę
ALTER TABLE ${this.tableName}
ADD COLUMN ${oldName} ${await this.getColumnType(newName)};
-- Skopiuj dane z powrotem
UPDATE ${this.tableName}
SET ${oldName} = ${newName};
`
});
return steps;
}
// Wzorzec 2: Zmiana typu danych z walidacją
async changeColumnType(columnName, newType, converter) {
const steps = [];
const tempColumn = `${columnName}_new`;
// Krok 1: Dodaj kolumnę tymczasową
steps.push({
name: 'Dodaj kolumnę tymczasową',
up: `
ALTER TABLE ${this.tableName}
ADD COLUMN ${tempColumn} ${newType};
`,
down: `
ALTER TABLE ${this.tableName}
DROP COLUMN IF EXISTS ${tempColumn};
`
});
// Krok 2: Wypełnij przekonwertowanymi danymi
steps.push({
name: 'Konwertuj i wypełnij dane',
up: converter || `
UPDATE ${this.tableName}
SET ${tempColumn} = ${columnName}::${newType};
`,
validation: async () => {
// Sprawdź błędy konwersji
const result = await this.client.query(`
SELECT COUNT(*) FROM ${this.tableName}
WHERE ${columnName} IS NOT NULL
AND ${tempColumn} IS NULL
`);
return result.rows[0].count === '0';
}
});
// Krok 3: Stwórz trigger synchronizacji
steps.push({
name: 'Synchronizuj nowe zapisy',
up: `
CREATE OR REPLACE FUNCTION sync_${columnName}_type_change()
RETURNS TRIGGER AS $$
BEGIN
NEW.${tempColumn} := NEW.${columnName}::${newType};
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_sync_${columnName}_type
BEFORE INSERT OR UPDATE ON ${this.tableName}
FOR EACH ROW
EXECUTE FUNCTION sync_${columnName}_type_change();
`,
down: `
DROP TRIGGER IF EXISTS trigger_sync_${columnName}_type ON ${this.tableName};
DROP FUNCTION IF EXISTS sync_${columnName}_type_change();
`
});
// Krok 4: Atomowa zamiana
steps.push({
name: 'Zamień kolumny',
up: `
BEGIN;
-- Usuń starą kolumnę
ALTER TABLE ${this.tableName}
DROP COLUMN ${columnName};
-- Zmień nazwę nowej kolumny
ALTER TABLE ${this.tableName}
RENAME COLUMN ${tempColumn} TO ${columnName};
-- Wyczyść
DROP TRIGGER IF EXISTS trigger_sync_${columnName}_type ON ${this.tableName};
DROP FUNCTION IF EXISTS sync_${columnName}_type_change();
COMMIT;
`,
down: `
-- Trzeba by odwrócić zmianę typu
ALTER TABLE ${this.tableName}
ALTER COLUMN ${columnName} TYPE ${await this.getColumnType(columnName)}
USING ${columnName}::${await this.getColumnType(columnName)};
`
});
return steps;
}
// Wzorzec 3: Podział tabeli
async splitTable(newTables) {
const steps = [];
// Krok 1: Stwórz nowe tabele
for (const newTable of newTables) {
steps.push({
name: `Stwórz tabelę ${newTable.name}`,
up: newTable.createStatement,
down: `DROP TABLE IF EXISTS ${newTable.name} CASCADE;`
});
}
// Krok 2: Stwórz widoki dla kompatybilności wstecznej
steps.push({
name: 'Stwórz widok kompatybilności',
up: `
CREATE OR REPLACE VIEW ${this.tableName}_compat AS
SELECT
${newTables.map(t => t.columns.map(c => `${t.alias}.${c}`).join(', ')).join(',\n ')}
FROM ${newTables[0].name} ${newTables[0].alias}
${newTables.slice(1).map(t => `
JOIN ${t.name} ${t.alias} ON ${t.alias}.${t.joinColumn} = ${newTables[0].alias}.id
`).join('')};
`,
down: `DROP VIEW IF EXISTS ${this.tableName}_compat;`
});
// Krok 3: Stwórz triggery instead-of
steps.push({
name: 'Stwórz triggery zapisu',
up: `
CREATE OR REPLACE FUNCTION ${this.tableName}_insert_trigger()
RETURNS TRIGGER AS $$
DECLARE
${newTables.map(t => `${t.name}_id INTEGER;`).join('\n ')}
BEGIN
-- Wstaw do podzielonych tabel
${newTables.map((t, i) => `
INSERT INTO ${t.name} (${t.columns.join(', ')})
VALUES (${t.columns.map(c => `NEW.${c}`).join(', ')})
RETURNING id INTO ${t.name}_id;
`).join('\n')}
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER ${this.tableName}_instead_insert
INSTEAD OF INSERT ON ${this.tableName}_compat
FOR EACH ROW
EXECUTE FUNCTION ${this.tableName}_insert_trigger();
`,
down: `
DROP TRIGGER IF EXISTS ${this.tableName}_instead_insert ON ${this.tableName}_compat;
DROP FUNCTION IF EXISTS ${this.tableName}_insert_trigger();
`
});
return steps;
}
async getColumnType(columnName) {
const result = await this.client.query(`
SELECT data_type, character_maximum_length, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_name = $1 AND column_name = $2
`, [this.tableName, columnName]);
const col = result.rows[0];
if (col.character_maximum_length) {
return `${col.data_type}(${col.character_maximum_length})`;
} else if (col.numeric_precision) {
return `${col.data_type}(${col.numeric_precision},${col.numeric_scale})`;
}
return col.data_type;
}
}
> Stwórz migrację danych do:
> - Normalizacji zdenormalizowanych danych do właściwych tabel
> - Inteligentnego łączenia duplikatów rekordów
> - Transformacji legacy formatów do nowej struktury
> - Zachowania integralności danych i relacji
data-migration.js
class DataMigration {
constructor(client) {
this.client = client;
this.batchSize = 1000;
}
// Normalizuj zdenormalizowane dane
async normalizeDenormalizedData() {
console.log('Rozpoczynam normalizację danych...');
// Przykład: Wyciągnij adresy z tabeli users
await this.client.query('BEGIN');
try {
// Stwórz tabelę adresów
await this.client.query(`
CREATE TABLE IF NOT EXISTS addresses (
id SERIAL PRIMARY KEY,
street_address VARCHAR(255),
city VARCHAR(100),
state VARCHAR(50),
postal_code VARCHAR(20),
country VARCHAR(2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(street_address, city, state, postal_code, country)
);
`);
// Stwórz tabelę łączącą user_addresses
await this.client.query(`
CREATE TABLE IF NOT EXISTS user_addresses (
user_id INTEGER REFERENCES users(id),
address_id INTEGER REFERENCES addresses(id),
address_type VARCHAR(20) DEFAULT 'primary',
is_primary BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, address_id, address_type)
);
`);
// Migruj adresy w partiach
let offset = 0;
let hasMore = true;
while (hasMore) {
const users = await this.client.query(`
SELECT id, street_address, city, state, postal_code, country
FROM users
WHERE street_address IS NOT NULL
ORDER BY id
LIMIT $1 OFFSET $2
`, [this.batchSize, offset]);
if (users.rows.length === 0) {
hasMore = false;
break;
}
for (const user of users.rows) {
// Wstaw lub pobierz adres
const addressResult = await this.client.query(`
INSERT INTO addresses (street_address, city, state, postal_code, country)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (street_address, city, state, postal_code, country)
DO UPDATE SET street_address = EXCLUDED.street_address
RETURNING id
`, [user.street_address, user.city, user.state, user.postal_code, user.country]);
// Połącz z użytkownikiem
await this.client.query(`
INSERT INTO user_addresses (user_id, address_id, address_type, is_primary)
VALUES ($1, $2, 'primary', true)
ON CONFLICT DO NOTHING
`, [user.id, addressResult.rows[0].id]);
}
console.log(`Przetworzono ${offset + users.rows.length} użytkowników...`);
offset += this.batchSize;
}
// Dodaj foreign key do tabeli users
await this.client.query(`
ALTER TABLE users
ADD COLUMN primary_address_id INTEGER REFERENCES addresses(id);
`);
// Zaktualizuj referencję głównego adresu
await this.client.query(`
UPDATE users u
SET primary_address_id = (
SELECT address_id
FROM user_addresses ua
WHERE ua.user_id = u.id
AND ua.is_primary = true
LIMIT 1
);
`);
await this.client.query('COMMIT');
console.log('Normalizacja danych zakończona pomyślnie');
} catch (error) {
await this.client.query('ROLLBACK');
throw error;
}
}
// Łącz duplikaty rekordów
async mergeDuplicates() {
console.log('Rozpoczynam łączenie duplikatów...');
// Znajdź duplikaty według email
const duplicates = await this.client.query(`
WITH duplicate_groups AS (
SELECT
email,
array_agg(id ORDER BY created_at ASC) as user_ids,
array_agg(created_at ORDER BY created_at ASC) as created_dates,
COUNT(*) as duplicate_count
FROM users
WHERE deleted_at IS NULL
GROUP BY email
HAVING COUNT(*) > 1
)
SELECT * FROM duplicate_groups
ORDER BY duplicate_count DESC
`);
console.log(`Znaleziono ${duplicates.rows.length} grup duplikatów`);
await this.client.query('BEGIN');
try {
for (const group of duplicates.rows) {
const [primaryId, ...duplicateIds] = group.user_ids;
console.log(`Łączę ${duplicateIds.length} duplikatów w użytkownika ${primaryId}`);
// Strategia łączenia: Zachowaj najstarsze konto, scal dane z innych
await this.mergeUserData(primaryId, duplicateIds);
}
await this.client.query('COMMIT');
console.log('Łączenie duplikatów zakończone pomyślnie');
} catch (error) {
await this.client.query('ROLLBACK');
throw error;
}
}
async mergeUserData(primaryId, duplicateIds) {
// Scal zamówienia
await this.client.query(`
UPDATE orders
SET user_id = $1
WHERE user_id = ANY($2::int[])
`, [primaryId, duplicateIds]);
// Scal adresy (unikając duplikatów)
await this.client.query(`
INSERT INTO user_addresses (user_id, address_id, address_type, is_primary)
SELECT $1, address_id, address_type, false
FROM user_addresses
WHERE user_id = ANY($2::int[])
ON CONFLICT DO NOTHING
`, [primaryId, duplicateIds]);
// Scal dane profilu (zachowaj wartości nie-null)
const profiles = await this.client.query(`
SELECT * FROM users
WHERE id = ANY($1::int[])
ORDER BY created_at DESC
`, [[primaryId, ...duplicateIds]]);
const mergedProfile = this.mergeProfiles(profiles.rows);
await this.client.query(`
UPDATE users
SET
first_name = COALESCE($2, first_name),
last_name = COALESCE($3, last_name),
phone = COALESCE($4, phone),
bio = COALESCE($5, bio),
preferences = COALESCE($6, preferences)
WHERE id = $1
`, [
primaryId,
mergedProfile.first_name,
mergedProfile.last_name,
mergedProfile.phone,
mergedProfile.bio,
mergedProfile.preferences
]);
// Soft delete duplikatów
await this.client.query(`
UPDATE users
SET
deleted_at = CURRENT_TIMESTAMP,
deleted_by = $1,
email = email || '_deleted_' || id
WHERE id = ANY($2::int[])
`, [primaryId, duplicateIds]);
// Stwórz log audytu
await this.client.query(`
INSERT INTO audit_logs (table_name, record_id, action, user_id, changes, metadata)
VALUES ('users', $1, 'merge', $1, $2, $3)
`, [
primaryId,
JSON.stringify({ merged_ids: duplicateIds }),
JSON.stringify({
reason: 'duplicate_email_merge',
duplicate_count: duplicateIds.length
})
]);
}
mergeProfiles(profiles) {
const merged = {};
// Priorytet: wartości nie-null od najnowszych do najstarszych
const fields = ['first_name', 'last_name', 'phone', 'bio'];
for (const field of fields) {
for (const profile of profiles) {
if (profile[field] && !merged[field]) {
merged[field] = profile[field];
break;
}
}
}
// Scal preferencje JSON
merged.preferences = {};
for (const profile of profiles.reverse()) {
if (profile.preferences) {
merged.preferences = { ...merged.preferences, ...profile.preferences };
}
}
return merged;
}
// Przekształć legacy formaty
async transformLegacyData() {
console.log('Rozpoczynam transformację legacy danych...');
// Przykład: Przekształć starą strukturę JSON do nowej znormalizowanej struktury
const legacyRecords = await this.client.query(`
SELECT id, legacy_data
FROM legacy_table
WHERE migrated = false
ORDER BY id
`);
const transformer = new LegacyDataTransformer();
await this.client.query('BEGIN');
try {
for (const record of legacyRecords.rows) {
const transformed = transformer.transform(record.legacy_data);
// Wstaw do nowej struktury
await this.insertTransformedData(transformed);
// Oznacz jako zmigrowane
await this.client.query(`
UPDATE legacy_table
SET migrated = true, migrated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [record.id]);
}
await this.client.query('COMMIT');
console.log('Transformacja legacy danych zakończona');
} catch (error) {
await this.client.query('ROLLBACK');
throw error;
}
}
}
class LegacyDataTransformer {
transform(legacyData) {
// Przykład logiki transformacji
const transformed = {
user: {
email: legacyData.email || legacyData.user_email,
name: this.parseName(legacyData.name || legacyData.full_name),
created_at: this.parseDate(legacyData.date_created || legacyData.created)
},
profile: {
bio: legacyData.description || legacyData.about,
avatar_url: this.normalizeUrl(legacyData.avatar || legacyData.profile_image),
preferences: this.parsePreferences(legacyData.settings || {})
},
addresses: this.parseAddresses(legacyData.addresses || legacyData.locations || [])
};
return transformed;
}
parseName(nameString) {
if (!nameString) return { first_name: null, last_name: null };
const parts = nameString.trim().split(/\s+/);
if (parts.length === 1) {
return { first_name: parts[0], last_name: null };
}
return {
first_name: parts[0],
last_name: parts.slice(1).join(' ')
};
}
parseDate(dateValue) {
if (!dateValue) return null;
// Obsłuż różne legacy formaty dat
const date = new Date(dateValue);
return isNaN(date.getTime()) ? null : date.toISOString();
}
normalizeUrl(url) {
if (!url) return null;
// Dodaj protokół jeśli brakuje
if (!url.match(/^https?:\/\//)) {
url = 'https://' + url;
}
try {
new URL(url); // Walidacja
return url;
} catch {
return null;
}
}
parsePreferences(settings) {
// Przekształć starą strukturę ustawień do nowych preferencji
return {
notifications: {
email: settings.email_notifications !== false,
push: settings.push_notifications === true,
sms: settings.sms_notifications === true
},
privacy: {
profile_visible: settings.public_profile !== false,
show_email: settings.show_email === true
},
theme: settings.theme || 'light'
};
}
parseAddresses(addresses) {
if (!Array.isArray(addresses)) {
addresses = [addresses].filter(Boolean);
}
return addresses.map((addr, index) => ({
street_address: addr.street || addr.address1,
city: addr.city,
state: addr.state || addr.province,
postal_code: addr.zip || addr.postal_code,
country: addr.country || 'PL',
is_primary: index === 0
}));
}
}
> Migruj naszą aplikację z:
> - React 17 do React 18 z funkcjami współbieżnymi
> - Express 4 do Express 5
> - Webpack 4 do Webpack 5
> Dołącz codemods, naprawy kompatybilności i testowanie
framework-migration.js
class FrameworkMigration {
constructor(projectPath) {
this.projectPath = projectPath;
}
// Migracja React 17 do 18
async migrateReact17to18() {
const steps = [];
// Krok 1: Zaktualizuj zależności
steps.push({
name: 'Zaktualizuj pakiety React',
command: `
npm update react@^18.0.0 react-dom@^18.0.0
npm update @types/react@^18.0.0 @types/react-dom@^18.0.0 --save-dev
`,
validation: async () => {
const pkg = require(path.join(this.projectPath, 'package.json'));
return pkg.dependencies.react.startsWith('18');
}
});
// Krok 2: Zaktualizuj wywołania ReactDOM.render
steps.push({
name: 'Zaktualizuj renderowanie root',
codemod: async () => {
const files = await this.findFiles('**/*.{js,jsx,ts,tsx}');
for (const file of files) {
let content = await fs.readFile(file, 'utf8');
let modified = false;
// Zaktualizuj ReactDOM.render do createRoot
if (content.includes('ReactDOM.render')) {
content = content.replace(
/ReactDOM\.render\s*\(\s*<(.+?)\/>,\s*document\.getElementById\(['"](.+?)['"]\)\s*\)/g,
(match, component, id) => {
modified = true;
return `
const root = ReactDOM.createRoot(document.getElementById('${id}'));
root.render(<${component}/>);`;
}
);
}
// Zaktualizuj hydrate do hydrateRoot
if (content.includes('ReactDOM.hydrate')) {
content = content.replace(
/ReactDOM\.hydrate\s*\(\s*<(.+?)\/>,\s*document\.getElementById\(['"](.+?)['"]\)\s*\)/g,
(match, component, id) => {
modified = true;
return `
const root = ReactDOM.hydrateRoot(document.getElementById('${id}'), <${component}/>);`;
}
);
}
if (modified) {
await fs.writeFile(file, content);
console.log(`Zaktualizowano: ${file}`);
}
}
}
});
// Krok 3: Dodaj granice Suspense
steps.push({
name: 'Dodaj granice Suspense dla funkcji współbieżnych',
codemod: async () => {
const componentFiles = await this.findFiles('**/components/**/*.{jsx,tsx}');
for (const file of componentFiles) {
let content = await fs.readFile(file, 'utf8');
// Znajdź lazy-loaded komponenty
if (content.includes('React.lazy')) {
// Sprawdź czy Suspense jest już zaimportowane
if (!content.includes('Suspense')) {
content = content.replace(
/import\s+React(.+?)from\s+['"]react['"]/,
"import React$1, { Suspense } from 'react'"
);
}
// Owij lazy komponenty w Suspense
content = content.replace(
/<(\w+)\s+\/>/g,
(match, component) => {
if (content.includes(`const ${component} = React.lazy`)) {
return `<Suspense fallback={<div>Ładowanie...</div>}>
${match}
</Suspense>`;
}
return match;
}
);
await fs.writeFile(file, content);
}
}
}
});
// Krok 4: Zaktualizuj użycie StrictMode
steps.push({
name: 'Zaktualizuj StrictMode dla React 18',
notes: `
React 18's StrictMode ma dodatkowe sprawdzenia:
- Komponenty będą się remountować w development
- Efekty będą się ponownie uruchamiać
- Zaktualizuj kod, który polega na pojedynczym mounting
`
});
return steps;
}
// Migracja Express 4 do 5
async migrateExpress4to5() {
const steps = [];
// Krok 1: Zaktualizuj zależności
steps.push({
name: 'Zaktualizuj Express',
command: 'npm install express@^5.0.0',
validation: async () => {
const pkg = require(path.join(this.projectPath, 'package.json'));
return pkg.dependencies.express.startsWith('5');
}
});
// Krok 2: Zaktualizuj middleware
steps.push({
name: 'Zaktualizuj usunięte middleware',
codemod: async () => {
const files = await this.findFiles('**/*.js');
for (const file of files) {
let content = await fs.readFile(file, 'utf8');
let modified = false;
// Zamień app.param na router.param
if (content.includes('app.param')) {
content = content.replace(/app\.param/g, 'router.param');
modified = true;
}
// Zaktualizuj obsługę błędów
if (content.includes('app.use(function(err')) {
console.log(`Uwaga: Error handlers w ${file} mogą wymagać aktualizacji dla Express 5`);
}
if (modified) {
await fs.writeFile(file, content);
}
}
}
});
// Krok 3: Zaktualizuj usunięte metody
steps.push({
name: 'Zaktualizuj przestarzałe metody',
codemod: async () => {
const replacements = {
'req.param()': 'req.params, req.body, lub req.query',
'res.send(status)': 'res.sendStatus(status)',
'res.redirect(url, status)': 'res.redirect(status, url)'
};
const files = await this.findFiles('**/routes/**/*.js');
for (const file of files) {
let content = await fs.readFile(file, 'utf8');
let modified = false;
for (const [old, replacement] of Object.entries(replacements)) {
if (content.includes(old)) {
console.log(`Ostrzeżenie: ${file} używa ${old}, zamień na ${replacement}`);
modified = true;
}
}
if (modified) {
// Dodaj komentarze TODO
content = '// TODO: Zaktualizuj dla kompatybilności Express 5\n' + content;
await fs.writeFile(file, content);
}
}
}
});
return steps;
}
// Migracja Webpack 4 do 5
async migrateWebpack4to5() {
const steps = [];
// Krok 1: Zaktualizuj webpack i powiązane pakiety
steps.push({
name: 'Zaktualizuj pakiety Webpack',
command: `
npm install webpack@^5.0.0 webpack-cli@^4.0.0 webpack-dev-server@^4.0.0
npm install html-webpack-plugin@^5.0.0 mini-css-extract-plugin@^2.0.0
`
});
// Krok 2: Zaktualizuj konfigurację webpack
steps.push({
name: 'Zaktualizuj webpack.config.js',
codemod: async () => {
const configPath = path.join(this.projectPath, 'webpack.config.js');
let config = await fs.readFile(configPath, 'utf8');
// Zaktualizuj polyfille node
if (config.includes('node:')) {
config = config.replace(
/node:\s*{[^}]+}/,
`node: false // Polyfille Node usunięte w Webpack 5`
);
// Dodaj fallbacks jeśli potrzebne
config = config.replace(
/module\.exports\s*=\s*{/,
`module.exports = {
resolve: {
fallback: {
"path": require.resolve("path-browserify"),
"fs": false,
"crypto": require.resolve("crypto-browserify"),
"stream": require.resolve("stream-browserify"),
"buffer": require.resolve("buffer/")
}
},`
);
}
// Zaktualizuj moduły assetów
config = config.replace(
/file-loader|url-loader/g,
'asset/resource'
);
// Zaktualizuj optymalizację
if (!config.includes('optimization')) {
config = config.replace(
/module\.exports\s*=\s*{/,
`module.exports = {
optimization: {
usedExports: true,
sideEffects: false,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
}
}
}
},`
);
}
await fs.writeFile(configPath, config);
}
});
// Krok 3: Zaktualizuj importy
steps.push({
name: 'Zaktualizuj dynamiczne importy',
codemod: async () => {
const files = await this.findFiles('**/*.{js,jsx,ts,tsx}');
for (const file of files) {
let content = await fs.readFile(file, 'utf8');
// Zaktualizuj magic comments
content = content.replace(
/\/\*\s*webpackChunkName:\s*"([^"]+)"\s*\*\//g,
'/* webpackChunkName: "$1", webpackPrefetch: true */'
);
await fs.writeFile(file, content);
}
}
});
return steps;
}
async findFiles(pattern) {
const glob = require('glob');
return new Promise((resolve, reject) => {
glob(pattern, { cwd: this.projectPath }, (err, files) => {
if (err) reject(err);
else resolve(files.map(f => path.join(this.projectPath, f)));
});
});
}
}
> Stwórz workflow Git dla bezpiecznych migracji:
> - Branche funkcji dla każdego kroku migracji
> - Automatyczne testowanie na każdym etapie
> - Procedury rollback
> - Strategia stopniowego wdrażania
migration-git-workflow.js
class MigrationGitWorkflow {
constructor(repoPath) {
this.repoPath = repoPath;
this.git = simpleGit(repoPath);
}
async createMigrationWorkflow(migrationName) {
const workflow = {
branches: [],
pullRequests: [],
rollbackPlan: []
};
// Stwórz główny branch migracji
const mainBranch = `migration/${migrationName}`;
await this.git.checkoutBranch(mainBranch, 'main');
workflow.branches.push(mainBranch);
// Stwórz branche kroków
const steps = [
'schema-changes',
'compatibility-layer',
'data-migration',
'code-updates',
'cleanup'
];
for (const step of steps) {
const stepBranch = `${mainBranch}/${step}`;
await this.git.checkoutBranch(stepBranch, mainBranch);
workflow.branches.push(stepBranch);
// Stwórz szablon PR
workflow.pullRequests.push({
branch: stepBranch,
target: mainBranch,
title: `[${migrationName}] ${step}`,
template: this.generatePRTemplate(step),
checks: this.getRequiredChecks(step)
});
}
// Stwórz branche rollback
for (const step of steps) {
const rollbackBranch = `${mainBranch}/rollback-${step}`;
workflow.rollbackPlan.push({
step,
branch: rollbackBranch,
procedure: this.generateRollbackProcedure(step)
});
}
return workflow;
}
generatePRTemplate(step) {
return `
## Krok migracji: ${step}
### Zmiany
- [ ] Wylistuj wszystkie zmiany wykonane w tym kroku
### Testowanie
- [ ] Testy jednostkowe przechodzą
- [ ] Testy integracyjne przechodzą
- [ ] Migracja przetestowana na stagingu
- [ ] Rollback przetestowany
### Weryfikacja
- [ ] Integralność danych zweryfikowana
- [ ] Wpływ na wydajność zmierzony
- [ ] Brak breaking changes dla działających systemów
### Plan rollback
Opisz jak wycofać ten konkretny krok jeśli potrzeba
### Zależności
- Poprzednie kroki, które muszą być ukończone
- Systemy, które muszą być powiadomione
### Monitorowanie
- Metryki do obserwacji podczas wdrożenia
- Skonfigurowane alerty
`;
}
getRequiredChecks(step) {
const baseChecks = [
'ci/tests',
'ci/lint',
'ci/security'
];
const stepSpecificChecks = {
'schema-changes': ['db/migration-test', 'db/compatibility'],
'data-migration': ['data/integrity', 'data/performance'],
'code-updates': ['app/integration', 'app/smoke-tests']
};
return [
...baseChecks,
...(stepSpecificChecks[step] || [])
];
}
generateRollbackProcedure(step) {
const procedures = {
'schema-changes': `
1. Uruchom migrację w dół: npm run migrate:down -- --step=schema-changes
2. Wdróż poprzednią wersję schematu bazy danych
3. Zweryfikuj że wszystkie aplikacje mogą się połączyć
4. Sprawdź integralność danych
`,
'data-migration': `
1. Zatrzymaj proces migracji danych
2. Przywróć z backupu wykonanego przed migracją
3. Lub uruchom odwrotny skrypt migracji danych
4. Zweryfikuj spójność danych
`,
'code-updates': `
1. Wycofaj wdrożenie kodu
2. Wyczyść cache
3. Restartuj usługi
4. Zweryfikuj funkcjonalność
`
};
return procedures[step] || 'Standardowa procedura rollback';
}
async createMigrationTests() {
const testFile = `
// migration.test.js
const { MigrationRunner } = require('./migration-runner');
describe('Migracja: ${this.migrationName}', () => {
let runner;
let testDb;
beforeAll(async () => {
testDb = await createTestDatabase();
runner = new MigrationRunner(testDb);
});
afterAll(async () => {
await testDb.close();
});
describe('Migracja do przodu', () => {
test('powinna zakończyć się pomyślnie', async () => {
const result = await runner.migrate('up');
expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
});
test('powinna utrzymać integralność danych', async () => {
const before = await testDb.query('SELECT COUNT(*) FROM users');
await runner.migrate('up');
const after = await testDb.query('SELECT COUNT(*) FROM users');
expect(after.rows[0].count).toBe(before.rows[0].count);
});
test('powinna być idempotentna', async () => {
await runner.migrate('up');
const result = await runner.migrate('up');
expect(result.alreadyMigrated).toBe(true);
});
});
describe('Rollback', () => {
test('powinien się wycofać pomyślnie', async () => {
await runner.migrate('up');
const result = await runner.migrate('down');
expect(result.success).toBe(true);
});
test('powinien przywrócić oryginalny stan', async () => {
const snapshot = await createSnapshot(testDb);
await runner.migrate('up');
await runner.migrate('down');
const restored = await createSnapshot(testDb);
expect(restored).toEqual(snapshot);
});
});
describe('Wydajność', () => {
test('powinien zakończyć się w limicie czasu', async () => {
const start = Date.now();
await runner.migrate('up');
const duration = Date.now() - start;
expect(duration).toBeLessThan(300000); // 5 minut
});
test('nie powinien blokować tabel nadmiernie', async () => {
const locks = await monitorLocks(async () => {
await runner.migrate('up');
});
expect(locks.maxDuration).toBeLessThan(1000); // 1 sekunda
});
});
});
`;
await fs.writeFile(
path.join(this.repoPath, 'tests', 'migration.test.js'),
testFile
);
}
}
> Stwórz kompleksowe monitorowanie dla migracji:
> - Walidacja przed migracją
> - Śledzenie postępu w czasie rzeczywistym
> - Weryfikacja po migracji
> - Analiza wpływu na wydajność
migration-monitor.js
class MigrationMonitor {
constructor(config) {
this.config = config;
this.metrics = {
startTime: null,
endTime: null,
rowsProcessed: 0,
errors: [],
warnings: [],
performance: []
};
}
async preMigrationChecks() {
const checks = {
database: await this.checkDatabaseHealth(),
diskSpace: await this.checkDiskSpace(),
connections: await this.checkActiveConnections(),
backup: await this.verifyBackup(),
dependencies: await this.checkDependencies()
};
const failed = Object.entries(checks)
.filter(([_, result]) => !result.passed)
.map(([name, result]) => ({ name, ...result }));
if (failed.length > 0) {
throw new Error(`Sprawdzenia przed migracją nieudane: ${JSON.stringify(failed)}`);
}
return checks;
}
async checkDatabaseHealth() {
try {
// Sprawdź połączenie
await this.config.db.query('SELECT 1');
// Sprawdź opóźnienie replikacji
const lag = await this.config.db.query(`
SELECT
EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) as lag_seconds
`);
const lagSeconds = lag.rows[0]?.lag_seconds || 0;
return {
passed: lagSeconds < 10,
details: { replicationLag: lagSeconds }
};
} catch (error) {
return {
passed: false,
error: error.message
};
}
}
async checkDiskSpace() {
const { execSync } = require('child_process');
const output = execSync('df -h /var/lib/postgresql').toString();
const lines = output.split('\n');
const dataLine = lines[1];
const usage = parseInt(dataLine.split(/\s+/)[4]);
return {
passed: usage < 80,
details: { diskUsagePercent: usage }
};
}
startMigration(name) {
this.metrics.startTime = Date.now();
this.metrics.name = name;
// Rozpocznij monitorowanie
this.monitoringInterval = setInterval(() => {
this.collectMetrics();
}, 5000);
console.log(`Migracja rozpoczęta: ${name}`);
}
async collectMetrics() {
// Zbierz metryki bazy danych
const stats = await this.config.db.query(`
SELECT
(SELECT count(*) FROM pg_stat_activity) as connections,
(SELECT count(*) FROM pg_stat_activity WHERE state = 'active') as active_queries,
(SELECT count(*) FROM pg_locks WHERE granted = false) as waiting_locks,
pg_database_size(current_database()) as database_size
`);
this.metrics.performance.push({
timestamp: Date.now(),
...stats.rows[0]
});
// Sprawdź długo działające zapytania
const longQueries = await this.config.db.query(`
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 minutes'
AND state = 'active'
`);
if (longQueries.rows.length > 0) {
this.metrics.warnings.push({
type: 'long_running_query',
timestamp: Date.now(),
details: longQueries.rows
});
}
}
updateProgress(processed, total) {
this.metrics.rowsProcessed = processed;
this.metrics.totalRows = total;
const percentage = ((processed / total) * 100).toFixed(2);
const elapsed = (Date.now() - this.metrics.startTime) / 1000;
const rate = processed / elapsed;
const eta = (total - processed) / rate;
console.log(
`Postęp: ${processed}/${total} (${percentage}%) - ` +
`Tempo: ${rate.toFixed(0)}/sek - ETA: ${this.formatTime(eta)}`
);
}
recordError(error) {
this.metrics.errors.push({
timestamp: Date.now(),
message: error.message,
stack: error.stack,
context: error.context
});
console.error(`Błąd migracji: ${error.message}`);
}
async finishMigration() {
this.metrics.endTime = Date.now();
clearInterval(this.monitoringInterval);
// Uruchom weryfikację po migracji
const verification = await this.postMigrationVerification();
// Wygeneruj raport
const report = this.generateReport(verification);
// Zapisz raport
await this.saveReport(report);
return report;
}
async postMigrationVerification() {
const checks = {
dataIntegrity: await this.verifyDataIntegrity(),
constraints: await this.verifyConstraints(),
indexes: await this.verifyIndexes(),
performance: await this.verifyPerformance(),
application: await this.verifyApplicationHealth()
};
return checks;
}
async verifyDataIntegrity() {
// Uruchom sumy kontrolne lub walidacje liczby
const checks = [];
// Przykład: Zweryfikuj że liczby wierszy się zgadzają
if (this.config.validations?.rowCounts) {
for (const [table, expectedCount] of Object.entries(this.config.validations.rowCounts)) {
const result = await this.config.db.query(
`SELECT COUNT(*) as count FROM ${table}`
);
checks.push({
table,
expected: expectedCount,
actual: result.rows[0].count,
passed: result.rows[0].count === expectedCount
});
}
}
return {
passed: checks.every(c => c.passed),
details: checks
};
}
generateReport(verification) {
const duration = this.metrics.endTime - this.metrics.startTime;
return {
migration: this.metrics.name,
status: this.metrics.errors.length === 0 ? 'SUKCES' : 'NIEUDANA',
startTime: new Date(this.metrics.startTime).toISOString(),
endTime: new Date(this.metrics.endTime).toISOString(),
duration: this.formatTime(duration / 1000),
rowsProcessed: this.metrics.rowsProcessed,
errors: this.metrics.errors,
warnings: this.metrics.warnings,
verification,
performance: {
averageRate: this.metrics.rowsProcessed / (duration / 1000),
peakConnections: Math.max(...this.metrics.performance.map(p => p.connections)),
peakActiveQueries: Math.max(...this.metrics.performance.map(p => p.active_queries))
}
};
}
formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hours}g ${minutes}m ${secs}s`;
}
async saveReport(report) {
const filename = `migration-report-${report.migration}-${Date.now()}.json`;
await fs.writeFile(filename, JSON.stringify(report, null, 2));
console.log(`Raport zapisany do: ${filename}`);
}
}
// Przykład użycia
async function runMigrationWithMonitoring() {
const monitor = new MigrationMonitor({
db: databaseConnection,
validations: {
rowCounts: {
users: 10000,
orders: 50000
}
}
});
try {
// Sprawdzenia wstępne
await monitor.preMigrationChecks();
// Rozpocznij monitorowanie
monitor.startMigration('add-user-roles');
// Uruchom migrację
const migrator = new DatabaseMigrator(databaseConnection);
await migrator.runMigration((progress, total) => {
monitor.updateProgress(progress, total);
});
// Zakończ i zweryfikuj
const report = await monitor.finishMigration();
if (report.status === 'SUKCES') {
console.log('Migracja zakończona pomyślnie!');
} else {
console.error('Migracja nieudana!', report.errors);
}
} catch (error) {
monitor.recordError(error);
const report = await monitor.finishMigration();
throw error;
}
}

Nauczyłeś się, jak wykorzystać Claude Code do kompleksowego rozwoju i wykonywania migracji. Kluczem jest traktowanie migracji jako kodu pierwszej klasy - dokładnie zaplanowanego, starannie przetestowanego i bezpiecznie odwracalnego. Buduj migracje, które dają ci pewność do ewolucji systemów bez strachu.

Pamiętaj: Najlepsza migracja to ta, którą możesz uruchomić w piątek po południu bez utraty snu przez weekend. Używaj Claude Code do budowania migracji, które są przewidywalne, monitorowalne i odwracalne, zapewniając płynną ewolucję twoich danych i systemów wraz z potrzebami biznesowymi.