Migration
Quando usar: toda mudança de schema. Durante uma troca de release convivem a release N e a N-1 — as duas no mesmo banco. Por isso a regra é expand-only: release nova só acrescenta.
Expand-only na prática
| Permitido | Proibido |
|---|---|
CREATE TABLE IF NOT EXISTS | DROP TABLE, DROP COLUMN |
ALTER TABLE … ADD COLUMN … NULL | ALTER TABLE … MODIFY … NOT NULL numa coluna que a N-1 escreve |
| Índice novo | Renomear coluna ou tabela |
| Tabela nova para o recurso novo | Reescrever dado da N-1 na mesma migration |
O rollback é a release N-1 rodando sobre o schema novo: ela ignora a coluna a mais. Por isso a coluna nova é anulável e o código novo tolera o valor antigo (o default vive na aplicação, não no schema).
O arquivo e a lista
nns/internal/store/migrations/0001_sample_items.up.sql:
-- sample@0.1.0 — itens do módulo sample, tenant-scoped: toda consulta e toda
-- mutação filtram por tenant_id (o tenant vem do ctx VERIFICADO, nunca do
-- browser). O nome é único por tenant sem diferenciar maiúsculas (collation
-- ai_ci). A varredura é uma Action (nn_actions, do SDK): a tabela guarda só a
-- última (scan_action_id) e o resultado registrado pelo Runner.
--
-- Expand-only (CONTRACTS §3.3.4): release nova só ACRESCENTA; o rollback
-- nunca roda um "down". Por isso o CREATE TABLE é IF NOT EXISTS e a coluna
-- nova entra num arquivo NOVO, anulável.
CREATE TABLE IF NOT EXISTS sample_items (
tenant_id VARCHAR(20) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
item_id CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
name VARCHAR(80) NOT NULL,
description VARCHAR(500) NOT NULL,
created_by VARCHAR(300) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
created_release_id VARCHAR(105) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
scan_action_id CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
scan_status VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT 'none',
scan_engine VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin NULL,
scan_findings JSON NULL,
scan_score TINYINT UNSIGNED NULL,
scanned_at DATETIME(3) NULL,
scanned_by VARCHAR(105) CHARACTER SET ascii COLLATE ascii_bin NULL,
PRIMARY KEY (tenant_id, item_id),
UNIQUE KEY uq_sample_items_name (tenant_id, name),
KEY ix_sample_items_created (tenant_id, created_at, item_id),
KEY ix_sample_items_scan (scan_status, updated_at),
CONSTRAINT ck_sample_items_scan_status CHECK (scan_status IN ('none', 'pending', 'running', 'succeeded', 'failed'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
O nome do arquivo segue <NNNN>_<assunto>.up.sql. O MANIFEST é a lista fechada do que a Source
publica — o migrador lê por lista, nunca listando o classpath:
java/src/main/resources/br/com/nnumbers/console/sample/store/migrations/MANIFEST
# As migrations da Source "sample" (lidas do lugar: nns/internal/store/migrations, pelo <resources> do pom).
# Release nova só ACRESCENTA arquivo; SampleMigrationsTest compara esta lista com o diretório.
0001_sample_items.up.sql
Os arquivos não são copiados para dentro de src/: o pom.xml os lê do lugar e a lista do classpath
é o MANIFEST:
<resource>
<directory>${nn.sample.root}/nns/internal/store/migrations</directory>
<targetPath>br/com/nnumbers/console/sample/store/migrations</targetPath>
<filtering>false</filtering>
<includes>
<include>*.sql</include>
</includes>
</resource>
A ordem das Sources
java/src/main/java/.../store/SampleMigrations.java:
public static Migrator migrator() {
return Migrator.of(
MigrationSource.sdk(Migrator.SOURCE_REPLAY),
MigrationSource.sdk(Migrator.SOURCE_AUDIT),
MigrationSource.sdk(Migrator.SOURCE_IDEMPOTENCY),
MigrationSource.sdk(Migrator.SOURCE_ACTION),
MigrationSource.ofClasspath(SOURCE, SampleMigrations.class.getClassLoader(), RESOURCES));
}
As tabelas do SDK (replay de token, outbox de audit, idempotência, Actions) vêm antes das suas: o que o runtime usa para funcionar precisa existir antes do primeiro uso.
O nome da Source no ledger é o service.id (sample), e a versão é a ordem do arquivo
(0001 → 1). O mesmo migrador serve o Job migrate e o check schema do /ready.
Os grants da conta de runtime
A conta de runtime recebe DML por tabela, e nada de DDL:
public static final List<TableGrant> GRANTS =
List.of(TableGrant.of("sample_items", "SELECT", "INSERT", "UPDATE", "DELETE"));
A conta de migração (outro slot, db-migrator) é a única com DDL, e só existe durante o Job: se ela
vazar, o dano é limitado ao schema; se a de runtime vazar, o dano é limitado às linhas do módulo.
Checando o efeito
@Test
void migrateIsIdempotent() {
// O Job da release roda de novo a cada deploy: a segunda passada aplica ZERO migration.
assertThat(SampleMigrations.migrator().apply(CTX, db)).isZero();
}
@Test
void ledgerRecordsThePublishedMigrationWithItsChecksum() {
List<String> rows =
db.query(CTX,
"SELECT source, version, checksum FROM nn_schema_migrations WHERE source = ? ORDER BY version",
r -> r.string(1) + "/" + r.string(2) + "/" + r.string(3), "sample");
assertThat(rows).hasSize(1).allSatisfy(row -> assertThat(row).startsWith("sample/"));
}
E, em JDK puro, o hash congelado do arquivo publicado — mudar um .sql já aplicado quebra o build
em vez de quebrar o deploy:
private static final String FROZEN_0001 =
"1ef8e05cd45468822d5df9d984a7054d9dca1208e6d541029cc5d12873d249f5";
@Test
void thePublishedMigrationIsFrozen() throws Exception {
assertThat(sha256(resource("0001_sample_items.up.sql"))).isEqualTo(FROZEN_0001);
}
@Test
void manifestListsExactlyTheRepositoryDirectory() throws Exception {
// O migrador lê por LISTA, nunca listando o classpath: um arquivo esquecido no MANIFEST não
// seria aplicado, e este teste é quem avisa.
assertThat(manifest).isEqualTo(onDisk);
}
Erros comuns
- Editar um arquivo já publicado — o ledger guarda o checksum e o migrador recusa: a release não sobe. Correção de schema é arquivo novo.
- Esquecer o
MANIFEST— o arquivo não é aplicado, e o erro só aparece em produção. O teste que compara oMANIFESTcom o diretório pega isso no build. - Coluna nova
NOT NULLsem default — a release N-1, que ainda está no ar, quebra ao inserir. - Migration que apaga ou renomeia — o rollback deixa de ser possível, e a N-1 passa a falhar.
- DDL na conta de runtime — o contrato é DML por tabela; DDL é do Job.
- Escrever SQL gigante numa migration sem
IF NOT EXISTS— o migrador é idempotente por ledger, mas um re-run manual em banco que já tem a tabela precisa poder passar. - Contar com um "down" — não existe: o rollback é a release anterior sobre o schema novo.
- Misturar dado e schema — dado de negócio não entra em migration.
Esta página ajudou?
Reportar um problema nesta páginaNão envie senhas, chaves, tokens ou dados de clientes.