Procesos ETL, patrones de Change Data Capture (CDC), mapping de datos entre plataformas, EBCDIC vs UTF-8, packed decimal, y estrategias de migracion.
La migracion de datos desde o hacia IBM i es uno de los desafios mas subestimados en proyectos de modernizacion. Los datos en IBM i tienen particularidades unicas: encoding EBCDIC, tipos packed decimal, estructuras de archivos fisicos con multiples miembros, y relaciones implicitas que no estan definidas como foreign keys.
La migracion puede ser de IBM i a otra plataforma (ej: migrar datos a PostgreSQL), de otra plataforma a IBM i (ej: consolidar datos), o entre IBM i (ej: migrar a PowerVS o consolidar LPARs).
Migracion unica (one-time)
Se migran todos los datos de una vez. Requiere ventana de downtime. Simple pero riesgoso para volumenes grandes.
Migracion incremental (trickle)
Se migra un lote inicial y luego se sincronizan los cambios con CDC. Minimiza downtime. Mas complejo de implementar.
Replicacion continua
Los datos se replican en tiempo real de forma permanente. No hay cutover, ambos sistemas coexisten indefinidamente.
El patron ETL es la base de la mayoria de las migraciones. Se extraen datos de IBM i, se transforman al formato del destino, y se cargan en la plataforma target. Cada fase tiene sus propias herramientas y consideraciones.
-- Exportar tabla completa a CSV via SQL
-- (usando CPYTOIMPF desde CL o query directo)
-- Metodo 1: Query directo via JDBC/ODBC
SELECT
CUSID,
TRIM(CUSNAM) AS customer_name,
TRIM(CUSADR) AS address,
CUSBAL AS balance,
CHAR(OPNDAT, ISO) AS open_date,
CASE CUSTS
WHEN 'A' THEN 'Active'
WHEN 'I' THEN 'Inactive'
WHEN 'C' THEN 'Closed'
END AS status
FROM PRODLIB.CUSTMAST
WHERE CUSTS <> 'C'
ORDER BY CUSID;
-- Metodo 2: Unload a archivo delimitado
CREATE TABLE QTEMP.EXPORT_CUSTOMERS AS (
SELECT CUSID, CUSNAM, CUSADR, CUSBAL
FROM PRODLIB.CUSTMAST
) WITH DATA;-- Exportar con CPYTOIMPF (metodo CL nativo)
CPYTOIMPF FROMFILE(PRODLIB/CUSTMAST)
TOSTMF('/home/exports/custmast.csv')
MBROPT(*REPLACE)
STMFCCSID(1208) -- UTF-8
RCDDLM(*CRLF)
DTAFMT(*DLM)
STRDLM(*DBLQ)
FLDDLM(',')
-- Exportar con SQL
RUNSQLSTM SRCSTMF('/home/scripts/export_cust.sql')
COMMIT(*NONE)
NAMING(*SQL)La transformacion aborda las diferencias fundamentales entre IBM i y las plataformas modernas: encoding (EBCDIC a UTF-8), tipos de datos (packed decimal a numeric), formatos de fecha, y limpieza de campos con trailing blanks.
import pyodbc
import pandas as pd
# Conectar y extraer
conn = pyodbc.connect('DSN=IBMi_PROD')
df = pd.read_sql("""
SELECT CUSID, CUSNAM, CUSADR, CUSBAL,
OPNDAT, CUSTS
FROM PRODLIB.CUSTMAST
""", conn)
# Transformaciones
df['customer_name'] = df['CUSNAM'].str.strip()
df['address'] = df['CUSADR'].str.strip()
df['balance'] = df['CUSBAL'].astype(float)
df['open_date'] = pd.to_datetime(
df['OPNDAT'], format='%Y%m%d'
)
df['status'] = df['CUSTS'].map({
'A': 'Active', 'I': 'Inactive', 'C': 'Closed'
})
# Validar
assert df['CUSID'].is_unique, "IDs duplicados!"
assert df['balance'].notna().all(), "Balances nulos!"
# Cargar en PostgreSQL
from sqlalchemy import create_engine
pg_engine = create_engine(
'postgresql://user:pass@host/db'
)
df.to_sql('customers', pg_engine,
if_exists='replace', index=False)CDC captura los cambios incrementales en los datos (inserts, updates, deletes) y los replica al sistema destino. En IBM i, la fuente natural de CDC son los journals, que registran cada operacion de escritura a nivel de registro.
ETL batch (full extract)
CDC (incremental)
-- Leer cambios de las ultimas 24 horas
SELECT
jrn.JOURNAL_ENTRY_TYPE AS operation,
jrn.ENTRY_TIMESTAMP AS change_time,
jrn.CUSID,
jrn.CUSNAM,
jrn.CUSBAL
FROM TABLE(
QSYS2.DISPLAY_JOURNAL(
JOURNAL_LIBRARY => 'MYLIB',
JOURNAL_NAME => 'MYJRN',
STARTING_TIMESTAMP =>
CURRENT_TIMESTAMP - 24 HOURS,
JOURNAL_ENTRY_TYPES => 'PT,UP,DL',
JOURNAL_CODES => 'R',
FILE_NAME => 'CUSTMAST'
)
) AS jrn
ORDER BY jrn.ENTRY_TIMESTAMP;
-- Tipos de operacion:
-- PT = Put (INSERT)
-- UP = Update
-- DL = Delete
-- Los campos contienen el AFTER image
-- (con IMAGES(*BOTH) tambien se obtiene el BEFORE)Precisely Connect CDC
Herramienta enterprise para CDC en IBM i. Lee journals en tiempo real y publica a Kafka, MQ, archivos, o bases de datos. Soporta transformacion inline y multiples targets.
Debezium + custom connector
Open source. Requiere un conector custom para leer journals de IBM i. Comunidad activa pero soporte limitado para IBM i especificamente.
Los tipos de datos de IBM i (DDS y SQL) no mapean directamente a tipos de otras bases de datos. Esta tabla muestra las equivalencias mas comunes.
IBM i usa EBCDIC (Extended Binary Coded Decimal Interchange Code) como encoding nativo, mientras que el mundo moderno usa ASCII/UTF-8. Esta diferencia es la fuente de muchos problemas de migracion, especialmente con caracteres especiales, acentos y simbolos de moneda.
EBCDIC (CCSID 37 / 284 / 500)
UTF-8 (CCSID 1208)
-- Ver el CCSID de una tabla
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME,
CCSID, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH
FROM QSYS2.SYSCOLUMNS
WHERE TABLE_SCHEMA = 'MYLIB'
AND TABLE_NAME = 'CUSTMAST';
-- Convertir datos al extraer
SELECT
CAST(CUSNAM AS VARCHAR(40) CCSID 1208) AS name_utf8,
CAST(CUSADR AS VARCHAR(60) CCSID 1208) AS addr_utf8
FROM MYLIB.CUSTMAST;
-- Cambiar CCSID de un archivo IFS
CHGATR OBJ('/home/exports/data.csv')
ATR(*CCSID)
VALUE(1208)ñ, acentos (á é í), y simbolos (€ £) se corrompen si no se especifica correctamente el CCSID en la extraccion. Siempre usar CCSID 1208 (UTF-8) al exportar.IBM i usa dos formatos numericos que no existen en la mayoria de las plataformas modernas: packed decimal y zoned decimal. Entender estos formatos es critico cuando se migran datos a nivel de bytes (archivos planos, save files).
Packed Decimal (tipo P)
Almacena 2 digitos por byte (excepto el ultimo que tiene 1 digito + signo). Un campo PACKED(7,2) como 12345.67 se almacena como: 01 23 45 67 0F(F = positivo, D = negativo). Ocupa ceil((precision+1)/2) bytes = 4 bytes para P(7,2).
Zoned Decimal (tipo S)
Almacena 1 digito por byte. El signo va en la zona del ultimo byte. Un campo ZONED(7,2) como 12345.67 se almacena como: F1 F2 F3 F4 F5 F6 F7(ultimo byte: F7 positivo, D7 negativo). Ocupa exactamente precision bytes.
def decode_packed_decimal(data: bytes, decimals: int) -> float:
"""Decodifica packed decimal de IBM i."""
result = 0
for i, byte in enumerate(data):
if i < len(data) - 1:
# Cada byte tiene 2 digitos
high = (byte >> 4) & 0x0F
low = byte & 0x0F
result = result * 100 + high * 10 + low
else:
# Ultimo byte: 1 digito + signo
high = (byte >> 4) & 0x0F
sign = byte & 0x0F
result = result * 10 + high
if sign in (0x0D, 0x0B): # D o B = negativo
result = -result
return result / (10 ** decimals)
# Ejemplo: b'\x01\x23\x45\x67\x0F' = 12345.67
value = decode_packed_decimal(
b'\x01\x23\x45\x67\x0F', decimals=2
)
print(value) # 12345.67La validacion es la fase mas critica y a menudo la mas descuidada. Un error en la migracion de datos puede pasar desapercibido durante meses si no se valida sistematicamente.
-- 1. Validar conteos -- En IBM i (origen): SELECT COUNT(*) AS total_origin FROM PRODLIB.CUSTMAST; -- En PostgreSQL (destino): SELECT COUNT(*) AS total_dest FROM public.customers; -- Deben coincidir! -- 2. Validar sumas de control -- En IBM i: SELECT COUNT(*) AS rows, SUM(CUSBAL) AS sum_balance, MIN(CUSBAL) AS min_balance, MAX(CUSBAL) AS max_balance, COUNT(DISTINCT CUSTS) AS distinct_status FROM PRODLIB.CUSTMAST; -- En destino: misma query, comparar resultados -- 3. Validar datos especificos (sampling) -- Comparar 100 registros aleatorios SELECT CUSID, CUSNAM, CUSBAL FROM PRODLIB.CUSTMAST ORDER BY RAND() FETCH FIRST 100 ROWS ONLY; -- 4. Detectar truncamientos SELECT CUSID, CUSNAM FROM PRODLIB.CUSTMAST WHERE LENGTH(TRIM(CUSNAM)) > 40; -- limite del destino -- 5. Detectar caracteres problematicos SELECT CUSID, CUSNAM, HEX(CUSNAM) AS hex_value FROM PRODLIB.CUSTMAST WHERE CUSNAM LIKE '%?%' -- caracteres no convertidos
Validaciones obligatorias
Validaciones recomendadas
Big Bang (cutover unico)
Trickle (incremental + CDC)
Migracion trickle: timeline tipico
Pre-migracion
Durante la migracion
Post-migracion