1. Definir o que tem de continuar verdadeiro
Uma migração de dados precisa de um contrato verificável. Começa pela unidade que o negócio reconhece: movimento, posição, saldo, carteira ou versão de um documento. Uma tabela pode ter o número esperado de linhas e ainda assim atribuir valores à carteira errada. Define a chave, os campos relevantes, a janela de dados, as regras de transformação e os consumidores que devem continuar a funcionar. Regista também quem decide sobre uma divergência e que evidência é necessária antes do cutover. O identificador do snapshot ou da fronteira de alterações deve acompanhar os resultados, para evitar comparar estados diferentes sem o perceber. Num projeto APS fictício, o objetivo é migrar o reporting de movimentos antes do fecho mensal. O negócio exige conservar valores por movimento, separar carteiras e entregar o relatório até à hora acordada. Essas três condições originam verificações diferentes: reconciliação de conteúdo, teste de acesso com identidades representativas e execução do processo consumidor. O sucesso de uma cópia não substitui nenhuma delas. Divide o plano em extração, transferência, transformação, carga, reconciliação e aceitação do consumidor, com entradas e saídas explícitas. Assim podes localizar a etapa que falhou e atribuir trabalho concreto, em vez de declarar apenas que a migração está a vermelho.
2. Proteger todo o percurso dos dados
Desenha a segurança sobre o percurso completo. A origem, o bucket intermédio, o dataset de destino e os resultados de validação podem conter informação sensível. Para cada cópia, identifica leitores, escritores, retenção e responsável. Usa dados sintéticos ou uma transformação aprovada quando um ensaio não precisa dos valores reais. A necessidade de comparar dados não autoriza automaticamente copiar toda a produção para desenvolvimento. Os relatórios de diferenças também merecem proteção: podem revelar exatamente os valores que as tabelas finais procuram restringir. Em BigQuery, uma view autorizada pode expor uma projeção sem conceder acesso direto à origem, mas tens de rever os outros caminhos de acesso do consumidor e quem pode alterar a definição da view. Não uses apenas um teste com a identidade do administrador como prova de isolamento. Na cifragem, um default CMEK configurado no dataset não altera as tabelas que já existiam. O inventário de aceitação precisa de distinguir a configuração por omissão da configuração de cada recurso anterior. Liga cada afirmação a um recurso e a uma observação: a expressão “dataset protegido” é demasiado ampla se não explicitar a cópia, a identidade e o controlo verificados.
3. Comparar estados e granularidades compatíveis
Escolhe primeiro o estado lógico a comparar. Se a origem continua a receber movimentos enquanto validas uma cópia anterior, diferenças podem representar alterações posteriores ou falhas reais. Precisas de uma fronteira comum, como um snapshot controlado ou uma posição de alterações documentada. Uma etiqueta igual nos dois ficheiros é uma declaração útil, mas não autentica a extração nem demonstra que o produtor cumpriu essa fronteira. Em BigQuery, a garantia de snapshot das transações não deve ser estendida a uma origem externa que muda durante a transação. Escolhe depois a granularidade. Uma chave de conta é insuficiente se existem vários movimentos por conta. Verifica unicidade antes de fazer correspondência entre linhas. Uma PRIMARY KEY NOT ENFORCED declarada em BigQuery não rejeita duplicados por ti. Duplicados iguais e duplicados contraditórios devem permanecer visíveis até existir uma regra de tratamento aprovada. Finalmente, combina verificações: contagem, conjuntos de chaves, valores por registo e agregados por dimensão relevante. No exemplo A=10 e B=20 contra A=11 e B=19, o total 30 passa e as duas linhas falham. O resultado correto conserva ambas as observações; não escolhe apenas a métrica favorável.
4. Tornar explícitas as equivalências permitidas
Define normalização campo a campo. Se 0017 e 17 identificam carteiras diferentes, uma conversão para inteiro perde informação. Se NULL significa desconhecido e texto vazio significa presente sem conteúdo, substituir ambos pelo mesmo marcador pode esconder erro. Um hash resume a representação que lhe foi entregue; não recupera diferenças descartadas antes do cálculo. Ferramentas de validação permitem transformações e comparações por campos, pelo que a configuração dessas regras faz parte da revisão e deve ficar versionada com o resultado. Para montantes, define moeda, escala e tolerância antes de executar a comparação. No exercício desta aula, os montantes são exatos até duas casas decimais e não existe conversão cambial. Somar EUR e USD num único total não demonstra preservação dos valores por moeda. Não arredondes uma divergência apenas para fazer o teste passar. Para datas, distingue um instante global de uma hora civil. 10:00Z e 11:00+01:00 representam o mesmo instante; retirar o offset destrói a informação que permite essa equivalência. Uma hora sem fuso exige contexto explícito. As regras do exercício rejeitam-na em vez de assumir o fuso do computador, mas um sistema real pode ter um contrato diferente que precisa de ser definido e testado.
5. Preparar portabilidade e dependências de migração
Portabilidade inclui formato, tipos, consultas, identidades e operação. Um ficheiro que sai do serviço ainda precisa de ser interpretado pelo consumidor. Para campos nested ou repeated, a exportação direta para CSV não preserva o desenho; escolhe um formato compatível ou uma transformação explícita e valida o resultado. Evita testar apenas uma linha simples quando o esquema permite arrays vazios, vários elementos e valores nulos. A tradução de SQL também precisa de metadata adequada e de testes semânticos: a ausência de erro de tradução não mede o resultado nem a duração do processo mensal. No inventário de dependências, pergunta que ciclos ficaram fora da janela observada. Duas semanas sem leitura não excluem uma tabela usada no fecho trimestral. Inclui relatórios, schedules, integrações e processos manuais que realmente dependem dos dados. Avalia também localização e recuperação separadamente. Escolher EU em BigQuery não demonstra redundância entre regiões. Não deduzas uma capacidade de desastre a partir do nome de uma localização. Para o plano do projeto, converte estes pontos em responsáveis, testes de aceitação e dependências de calendário. Uma migração incremental pode reduzir o âmbito de cada decisão, mas continua a precisar de fronteiras claras entre consumidores migrados e ainda dependentes da origem.
6. Executar a reconciliação local
O programa Python apresentado abaixo é um exercício original, sem chamadas a Google Cloud. Recebe JSON pela entrada padrão e escreve um relatório JSON. Cada lado contém snapshot, complete e rows. Cada linha exige id, currency, amount, at e reference. O id é a chave única deste contrato fictício. amount é texto decimal, com no máximo duas casas; o programa converte-o para um inteiro em cêntimos. at é um timestamp com segundos e offset explícito. reference pode ser texto ou null, conservando espaços e maiúsculas. Não são aceites campos omitidos nem extra. Guarda o código em run.py e prepara duas linhas por lado: A com 10.00 e B com 20.00 na origem; A com 11.00 e B com 19.00 no destino. Usa EUR, o mesmo instante e snapshot cut-17, declarando complete=true. Executa python3 run.py < caso.json. Deves obter contagens [2,2] e totais EUR 3000 em ambos os lados, mas equivalentUnderDeclaredContract=false e diferenças em cents para A e B. Corrige os montantes e repete. Depois, acrescenta um duplicado igual a ambos os lados: a comparação deve continuar a recusar equivalência por nonunique-keys. O exercício não escolhe arbitrariamente uma linha para esconder duplicados.
7. Explorar contraexemplos e limites
Cria quatro variantes do ficheiro. Primeiro, substitui a chave B por C apenas no destino: o relatório deve identificar B em missing e C em unexpected, mesmo com o mesmo total. Segundo, muda reference de null para texto vazio apenas num lado: deve surgir diferença nesse campo. Terceiro, representa o mesmo instante com Z num lado e com um offset equivalente no outro: essa diferença textual deve desaparecer depois da interpretação temporal. Quarto, conserva linhas iguais mas usa snapshots diferentes ou complete=false: os bloqueios de âmbito devem impedir uma conclusão positiva. Lê as saídas literalmente. O programa soma os registos recebidos por moeda e mostra duplicados; não prova completude do inventário. Os dois lados vazios e declarados completos podem ser equivalentes sob este contrato, mas isso não demonstra que a extração de produção devia estar vazia. snapshotAuthenticityVerified, accessControlsVerified e productionCutoverApproved permanecem false. Também não há um cliente BigQuery, implementação de DVT, medição de latência, análise de permissões ou garantia sobre sistemas remotos. O valor pedagógico é aprender a encontrar contraexemplos e a delimitar o que uma comparação prova. Antes de aplicar uma regra semelhante num projeto real, define o contrato dos tipos e recolhe evidência da origem dos dados.
8. Decidir o cutover e preparar o RUN
Prepara o comité com três blocos de informação: o que foi comparado e passou, o que divergiu e o que ainda não foi observado. Para cada lacuna, apresenta impacto no consumidor, responsável e próximo teste. Mantém os critérios aprovados visíveis. Se o destino já aceitou escritas, voltar a apontar leitores para uma origem congelada não garante preservação desses dados novos. O plano de reversão precisa de tratar essa fronteira e demonstrar como reconciliar o que ficou apenas no destino. Uma mudança de encaminhamento bem-sucedida não é uma validação de fidelidade. Na passagem ao RUN, entrega a versão das regras de comparação, exemplos de falha conhecidos, identificação das extrações, responsáveis por decisões sobre diferenças e procedimento de escalada. A equipa de suporte deve conseguir distinguir atraso de atualização, alteração de esquema, falha de acesso e erro de conteúdo. A aula seguinte deve aprofundar a ingestão e o processamento sem perder este contrato. Retém cinco perguntas para qualquer desenho: qual é a unidade comparada, qual é o estado lógico, que equivalências foram autorizadas, que caminhos de acesso existem e que consumidor foi realmente testado? Respostas explícitas tornam a arquitetura e a decisão operacional mais verificáveis.
"""Original offline DR exercise. Declared record equivalence is not migration approval."""
import json
import re
import sys
from collections import Counter
from datetime import datetime, timezone
def fields(value, expected):
if type(value) is not dict or set(value) != set(expected):
raise ValueError('Unexpected or missing fields')
def label(value):
if type(value) is not str or not value or value != value.strip():
raise ValueError('Expected a nonempty label without surrounding whitespace')
return value
def amount_cents(value):
# Exact integer arithmetic. No binary float conversion or implicit rounding.
if type(value) is not str or not re.fullmatch(r'-?(0|[1-9][0-9]{0,15})(\.[0-9]{1,2})?', value):
raise ValueError('Expected a bounded decimal string with at most two decimal places')
negative = value.startswith('-')
whole, _, fraction = value.lstrip('-').partition('.')
cents = int(whole) * 100 + int(fraction.ljust(2, '0'))
return -cents if negative else cents
def instant(value):
if type(value) is not str or not re.fullmatch(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})', value):
raise ValueError('Expected whole-second ISO timestamp with explicit offset')
if value.endswith('-00:00'):
raise ValueError('Unknown local offset is not an established instant')
if not value.endswith('Z'):
hours, minutes = map(int, value[-5:].split(':'))
if hours > 23 or minutes > 59:
raise ValueError('Invalid offset')
parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
return parsed.astimezone(timezone.utc).isoformat()
def dataset(value):
fields(value, ['snapshot', 'complete', 'rows'])
label(value['snapshot'])
if type(value['complete']) is not bool or type(value['rows']) is not list:
raise ValueError('Expected explicit completeness and row list')
rows = []
for row in value['rows']:
fields(row, ['id', 'currency', 'amount', 'at', 'reference'])
rid = label(row['id'])
if type(row['currency']) is not str or not re.fullmatch(r'[A-Z]{3}', row['currency']):
raise ValueError('Currency must be a three-letter code; no conversion is performed')
if row['reference'] is not None and type(row['reference']) is not str:
raise ValueError('Reference must be string or null')
rows.append({'id': rid, 'currency': row['currency'], 'cents': amount_cents(row['amount']),
'instant': instant(row['at']), 'reference': row['reference']})
return rows
def reconcile(value):
fields(value, ['source', 'target'])
left, right = dataset(value['source']), dataset(value['target'])
counts = [Counter(row['id'] for row in rows) for rows in [left, right]]
duplicates = {'source': sorted(k for k, n in counts[0].items() if n > 1),
'target': sorted(k for k, n in counts[1].items() if n > 1)}
maps = [{row['id']: row for row in rows if count[row['id']] == 1}
for rows, count in zip([left, right], counts)]
source_ids, target_ids = set(counts[0]), set(counts[1])
changed = []
for rid in sorted(set(maps[0]) & set(maps[1])):
differing = [k for k in ['currency', 'cents', 'instant', 'reference'] if maps[0][rid][k] != maps[1][rid][k]]
if differing:
changed.append({'id': rid, 'fields': differing})
totals = []
for rows in [left, right]:
result = {}
for row in rows:
result[row['currency']] = result.get(row['currency'], 0) + row['cents']
totals.append(dict(sorted(result.items())))
missing, extra = sorted(source_ids-target_ids), sorted(target_ids-source_ids)
blockers = []
if value['source']['snapshot'] != value['target']['snapshot']:
blockers.append('different-declared-snapshots')
if not value['source']['complete'] or not value['target']['complete']:
blockers.append('incomplete-declared-scope')
if any(duplicates.values()):
blockers.append('nonunique-keys')
if missing or extra or changed:
blockers.append('record-differences')
return {'equivalentUnderDeclaredContract': not blockers, 'blockers': blockers,
'missing': missing, 'unexpected': extra, 'changed': changed, 'duplicates': duplicates,
'rowCounts': [len(left), len(right)], 'totalsCentsByCurrency': totals,
'snapshotAuthenticityVerified': False, 'accessControlsVerified': False,
'productionCutoverApproved': False}
if __name__ == '__main__':
try:
result = reconcile(json.load(sys.stdin))
print(json.dumps(result, sort_keys=True))
except (ValueError, TypeError, OverflowError) as error:
print(json.dumps({'error': str(error)}))
sys.exit(2)
Duas linhas mudam de 10/20 para 11/19: contagem e total passam, mas ambas as chaves divergem.
Armadilhas comuns
Contagem como fidelidade; hash após normalização destrutiva; snapshot declarado como prova; acesso do administrador como acesso do consumidor.
Tópicos relacionados: Ingestão e reprocessamento · Modelação e armazenamento · Observabilidade de pipelines
Compara estados e granularidades compatíveis e conserva visíveis as diferenças e os limites da evidência.
Referência: Database migration concepts and principles · Current linked standard guide (document title v4.2); edition date unconfirmed (2026-09-30 inspection)