Migra banco de dados de SQLite para PostgreSQL

- Muda provider Prisma de sqlite para postgresql
- Remove dependencias SQLite (better-sqlite3, adapter)
- Atualiza Better Auth para provider postgresql
- Simplifica prisma.ts removendo adapter SQLite
- Atualiza stack.yml para usar PostgreSQL existente com 2 replicas
- Remove logica de rebuild better-sqlite3 do start-web.sh
- Adiciona script de migracao de dados SQLite -> PostgreSQL
- Atualiza healthcheck para testar PostgreSQL via Prisma
- Habilita start-first deploy para zero-downtime

Melhoria: permite multiplas replicas e deploys sem downtime.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-11 00:35:27 -03:00
parent fb97d9bec8
commit 33a59634e7
10 changed files with 362 additions and 223 deletions

View file

@ -0,0 +1,264 @@
#!/usr/bin/env node
/**
* Script de migracao SQLite -> PostgreSQL
* Executa com acesso ao SQLite e PostgreSQL para migrar todos os dados
*
* Uso:
* SQLITE_PATH=/path/to/db.sqlite POSTGRES_URL=postgresql://... node scripts/migrate-sqlite-to-postgres.mjs
*/
import Database from "better-sqlite3"
import pg from "pg"
const SQLITE_PATH = process.env.SQLITE_PATH || "/app/data/db.sqlite"
const POSTGRES_URL = process.env.POSTGRES_URL || process.env.DATABASE_URL
if (!POSTGRES_URL) {
console.error("ERRO: POSTGRES_URL ou DATABASE_URL e obrigatorio")
process.exit(1)
}
console.log("=".repeat(60))
console.log("MIGRACAO SQLite -> PostgreSQL")
console.log("=".repeat(60))
console.log(`SQLite: ${SQLITE_PATH}`)
console.log(`PostgreSQL: ${POSTGRES_URL.replace(/:[^:@]+@/, ":***@")}`)
console.log("")
// Ordem das tabelas respeitando foreign keys
const TABLES_ORDER = [
// Tabelas sem dependencias
"Team",
"SlaPolicy",
"Company",
// Tabelas com dependencias simples
"User",
"Queue",
"AuthUser",
// Tabelas com chave composta
"TeamMember",
// Tabelas principais
"Ticket",
// Tabelas dependentes de Ticket
"TicketEvent",
"TicketComment",
"TicketRating",
"TicketAccessToken",
// Tabelas de usuario
"NotificationPreferences",
// Tabelas de relatorios
"ReportExportSchedule",
"ReportExportRun",
// Tabelas de autenticacao
"AuthSession",
"AuthAccount",
"AuthInvite",
"AuthInviteEvent",
"AuthVerification",
]
let sqlite
let pgClient
async function connect() {
console.log("[1/4] Conectando aos bancos de dados...")
try {
sqlite = new Database(SQLITE_PATH, { readonly: true })
console.log(" SQLite: conectado")
} catch (error) {
console.error(" SQLite: ERRO -", error.message)
process.exit(1)
}
try {
pgClient = new pg.Client({ connectionString: POSTGRES_URL })
await pgClient.connect()
console.log(" PostgreSQL: conectado")
} catch (error) {
console.error(" PostgreSQL: ERRO -", error.message)
process.exit(1)
}
}
async function migrateTable(tableName) {
let rows
try {
rows = sqlite.prepare(`SELECT * FROM "${tableName}"`).all()
} catch (error) {
console.log(` ${tableName}: tabela nao existe no SQLite, pulando`)
return { table: tableName, migrated: 0, skipped: true }
}
if (rows.length === 0) {
console.log(` ${tableName}: 0 registros`)
return { table: tableName, migrated: 0 }
}
const columns = Object.keys(rows[0])
const quotedColumns = columns.map(c => `"${c}"`).join(", ")
const placeholders = columns.map((_, i) => `$${i + 1}`).join(", ")
const insertSql = `INSERT INTO "${tableName}" (${quotedColumns}) VALUES (${placeholders}) ON CONFLICT DO NOTHING`
let migrated = 0
let errors = 0
for (const row of rows) {
const values = columns.map(col => {
let val = row[col]
// Converter JSON string para objeto se necessario (PostgreSQL usa JSONB)
if (typeof val === "string") {
// Detectar campos JSON pelo conteudo
if ((val.startsWith("{") && val.endsWith("}")) || (val.startsWith("[") && val.endsWith("]"))) {
try {
val = JSON.parse(val)
} catch {
// Manter como string se nao for JSON valido
}
}
}
// SQLite usa 0/1 para boolean, PostgreSQL usa true/false
// Prisma ja trata isso, mas vamos garantir
if (val === 0 || val === 1) {
// Detectar campos boolean pelo nome
const booleanFields = ["emailVerified", "isLead", "isAvulso", "emailEnabled", "hasBranches", "privacyPolicyAccepted"]
if (booleanFields.includes(col)) {
val = val === 1
}
}
return val
})
try {
await pgClient.query(insertSql, values)
migrated++
} catch (error) {
errors++
if (errors <= 3) {
console.error(` Erro ao inserir em ${tableName}:`, error.message)
}
}
}
console.log(` ${tableName}: ${migrated}/${rows.length} registros migrados${errors > 0 ? ` (${errors} erros)` : ""}`)
return { table: tableName, migrated, total: rows.length, errors }
}
async function updateSequences() {
console.log("\n[3/4] Atualizando sequences do PostgreSQL...")
// PostgreSQL precisa atualizar sequences para auto-increment funcionar corretamente
// Mas como usamos CUIDs, isso geralmente nao e necessario
// Vamos apenas verificar se ha sequences e atualiza-las se existirem
const sequenceQuery = `
SELECT
t.relname as table_name,
a.attname as column_name,
pg_get_serial_sequence(t.relname::text, a.attname::text) as sequence_name
FROM pg_class t
JOIN pg_attribute a ON a.attrelid = t.oid
WHERE t.relkind = 'r'
AND pg_get_serial_sequence(t.relname::text, a.attname::text) IS NOT NULL
`
try {
const result = await pgClient.query(sequenceQuery)
for (const row of result.rows) {
const updateSeq = `
SELECT setval('${row.sequence_name}',
COALESCE((SELECT MAX("${row.column_name}") FROM "${row.table_name}"), 0) + 1, false)
`
try {
await pgClient.query(updateSeq)
console.log(` Sequence ${row.sequence_name} atualizada`)
} catch {
// Ignorar erros de sequence
}
}
} catch {
console.log(" Nenhuma sequence encontrada (usando CUIDs)")
}
}
async function validateMigration() {
console.log("\n[4/4] Validando migracao...")
const validation = []
for (const tableName of TABLES_ORDER) {
let sqliteCount = 0
let pgCount = 0
try {
const sqliteResult = sqlite.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get()
sqliteCount = sqliteResult?.count || 0
} catch {
// Tabela nao existe no SQLite
continue
}
try {
const pgResult = await pgClient.query(`SELECT COUNT(*) as count FROM "${tableName}"`)
pgCount = parseInt(pgResult.rows[0]?.count || 0)
} catch {
pgCount = 0
}
const match = sqliteCount === pgCount
validation.push({ table: tableName, sqlite: sqliteCount, postgres: pgCount, match })
const status = match ? "OK" : "DIFF"
console.log(` ${tableName}: SQLite=${sqliteCount}, PostgreSQL=${pgCount} [${status}]`)
}
const allMatch = validation.every(v => v.match)
return allMatch
}
async function main() {
await connect()
console.log("\n[2/4] Migrando tabelas...")
try {
await pgClient.query("BEGIN")
for (const tableName of TABLES_ORDER) {
await migrateTable(tableName)
}
await pgClient.query("COMMIT")
console.log("\n Transacao commitada com sucesso!")
} catch (error) {
await pgClient.query("ROLLBACK")
console.error("\nERRO: Rollback executado -", error.message)
process.exit(1)
}
await updateSequences()
const valid = await validateMigration()
console.log("\n" + "=".repeat(60))
if (valid) {
console.log("MIGRACAO CONCLUIDA COM SUCESSO!")
} else {
console.log("MIGRACAO CONCLUIDA COM DIFERENCAS (verifique os logs)")
}
console.log("=".repeat(60))
sqlite.close()
await pgClient.end()
process.exit(valid ? 0 : 1)
}
main().catch(error => {
console.error("Erro fatal:", error)
process.exit(1)
})