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.
Rewolucja migracji
Dział zatytułowany „Rewolucja 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.
Tradycyjne vs migracje wspomagane AI
Dział zatytułowany „Tradycyjne vs migracje wspomagane AI”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
Dzień 1: Analiza> Przeanalizuj obecny stan> Zidentyfikuj wszystkie zależności> Wygeneruj plan migracji
Dzień 2: Implementacja> Stwórz skrypty migracji> Dodaj procedury rollback> Zbuduj testy weryfikacyjne
Dzień 3: Walidacja> Przetestuj przypadki brzegowe> Zweryfikuj integralność danych> Symuluj obciążenie produkcyjne
Dzień 4: Wdrożenie bez przestojów> Wykonaj z pewnością siebie> Monitoruj w czasie rzeczywistym> Świętuj sukces
Migracje schematów baz danych
Dział zatytułowany „Migracje schematów baz danych”Kompleksowa ewolucja schematu
Dział zatytułowany „Kompleksowa ewolucja schematu”> 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:
-
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(`SELECTtable_name,column_name,data_type,is_nullable,column_defaultFROM information_schema.columnsWHERE table_schema = 'public'ORDER BY table_name, ordinal_position`);// Grupuj według tabeliconst 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(`SELECTschemaname,tablename,indexname,indexdefFROM pg_indexesWHERE schemaname = 'public'`);return result.rows;}async analyzeQueryPatterns() {// Analizuj pg_stat_statements jeśli dostępnetry {const result = await this.client.query(`SELECTquery,calls,mean_exec_time,total_exec_timeFROM pg_stat_statementsWHERE query NOT LIKE '%pg_%'ORDER BY total_exec_time DESCLIMIT 50`);return result.rows;} catch (error) {// Rozszerzenie może nie być zainstalowanereturn [];}}} -
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ólupQueries.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_permissionsupQueries.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 usersupQueries.push(`ALTER TABLE usersADD COLUMN role_id INTEGER REFERENCES roles(id);`);downQueries.push('ALTER TABLE users DROP COLUMN IF EXISTS role_id;');// Stwórz domyślne roleupQueries.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 indeksyupQueries.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 tabelfor (const tableName of Object.keys(this.analysis.tables)) {const table = this.analysis.tables[tableName];// Pomiń tabele systemowe i już soft-deletable tabeleif (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ówupQueries.push(`CREATE INDEX idx_${tableName}_not_deletedON ${tableName}(id)WHERE deleted_at IS NULL;`);downQueries.push(`DROP INDEX IF EXISTS idx_${tableName}_not_deleted;`);// Stwórz widok dla aktywnych rekordówupQueries.push(`CREATE OR REPLACE VIEW ${tableName}_active ASSELECT * 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 audytuupQueries.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 indeksyupQueries.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 tabelfor (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 istniejeif (!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 istniejeif (!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 istniejeif (!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 $$BEGINNEW.updated_at = CURRENT_TIMESTAMP;RETURN NEW;END;$$ LANGUAGE plpgsql;CREATE TRIGGER trigger_update_${tableName}_updated_atBEFORE UPDATE ON ${tableName}FOR EACH ROWEXECUTE 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 indeksyconst queryPatterns = this.analysis.currentQueries;for (const pattern of queryPatterns) {// Proste dopasowanie wzorców dla klauzul WHEREconst whereMatch = pattern.query.match(/WHERE\s+(\w+)\.?(\w+)\s*=/i);if (whereMatch) {const [_, tableAlias, column] = whereMatch;// Sprawdź czy indeks istniejeconst 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}_perfON ${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 migracjefor (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; ` }; } }
3. **Executor migracji**```javascriptclass 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(); } }}
Bezpieczne wzorce migracji
Dział zatytułowany „Bezpieczne wzorce migracji”> 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
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; }}
Wzorce migracji danych
Dział zatytułowany „Wzorce migracji danych”Złożone transformacje danych
Dział zatytułowany „Złożone transformacje danych”> 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
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 })); }}
Migracje frameworków
Dział zatytułowany „Migracje frameworków”Aktualizacje głównych wersji
Dział zatytułowany „Aktualizacje głównych wersji”> 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
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))); }); }); }}
Integracja z kontrolą wersji
Dział zatytułowany „Integracja z kontrolą wersji”Strategia branchowania migracji
Dział zatytułowany „Strategia branchowania migracji”> 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
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 rollbackOpisz 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-changes2. Wdróż poprzednią wersję schematu bazy danych3. Zweryfikuj że wszystkie aplikacje mogą się połączyć4. Sprawdź integralność danych`, 'data-migration': `1. Zatrzymaj proces migracji danych2. Przywróć z backupu wykonanego przed migracją3. Lub uruchom odwrotny skrypt migracji danych4. Zweryfikuj spójność danych`, 'code-updates': `1. Wycofaj wdrożenie kodu2. Wyczyść cache3. Restartuj usługi4. Zweryfikuj funkcjonalność` };
return procedures[step] || 'Standardowa procedura rollback'; }
async createMigrationTests() { const testFile = `// migration.test.jsconst { 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 ); }}
Monitorowanie i walidacja
Dział zatytułowany „Monitorowanie i walidacja”Sprawdzenia zdrowia migracji
Dział zatytułowany „Sprawdzenia zdrowia migracji”> Stwórz kompleksowe monitorowanie dla migracji:> - Walidacja przed migracją> - Śledzenie postępu w czasie rzeczywistym> - Weryfikacja po migracji> - Analiza wpływu na wydajność
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życiaasync 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; }}
Powiązane lekcje
Dział zatytułowany „Powiązane lekcje”Następne kroki
Dział zatytułowany „Następne kroki”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.