Ana içeriğe geç

Migration Rehberi

Veritabanı şema değişiklikleri Alembic ile yönetilir. Her migration production-critical'dir.

Son Migration'lar

RevisionTarihAçıklamaPatternRisk
0038_chk_device_source_ocpp2026-04-26chk_device_source whitelist'ine 'ocpp' ekleNOT VALID + VALIDATEDÜŞÜK (expand-only)
0037_add_ocpp_tables2026-04OCPP 1.6J şeması (8 tablo + hypertable)CREATEORTA
0036_merge_sofar_mqttMerge migration (multiple heads)mergeDÜŞÜK

0038_chk_device_source_ocpp (PR #277)

  • Revision id: 0038_chk_device_source_ocpp (27 char — alembic_version VARCHAR(32) limit altında)
  • Down revision: 0037_add_ocpp_tables
  • Constraint adı: ck_devices_chk_device_source (naming convention sonucu — bkz. aşağıda)
  • Pattern: NOT VALID + VALIDATE (production lock minimize)
  • Whitelist: ('modbus', 'zigbee', 'virtual', 'manual', 'ocpp') — eski 4 değer + 'ocpp'
  • Expand-only: Mevcut hiçbir satır reddedilmez; yeni whitelist eski whitelist'i kapsar.
  • Downgrade güvenliği: device_source='ocpp' kayıt varsa RuntimeError raise eder (cavalier DELETE yok — ocpp_chargers.device_id CASCADE + charge_sessions.charger_id RESTRICT zinciriyle çakışmayı önler). Operatör manuel temizlik yapmalı.
  • Gerekçe: 0037 migration'ı OCPP tablolarını ekledi ama chk_device_source whitelist'ine 'ocpp' eklemeyi atladı (test gap). Backend service.create_charger device_source="ocpp" insert ederken CheckViolationError → POST /api/ocpp/chargers HTTP 500 (production incident).
  • Detay troubleshooting: OCPP Charger Create 500.

Komutlar

# Yeni migration oluştur
cd backend && alembic revision --autogenerate -m "açıklama"

# Migration uygula
alembic upgrade head

# Son migration'ı geri al
alembic downgrade -1

# Mevcut durumu kontrol et
alembic current

Güvenli Migration Pattern'leri

Expand → Backfill → Contract

  1. Expand: Yeni kolon/tablo ekle (nullable veya default ile)
  2. Backfill: Mevcut verileri doldur
  3. Contract: Eski kolon/tabloyu kaldır (sonraki release'de)

Yeni Kolon Ekleme

# ✅ Doğru: nullable veya server_default ile
op.add_column('devices', sa.Column('device_role', sa.String(), nullable=True))

# ❌ Yanlış: NOT NULL direkt ekleme (mevcut satırlar hata verir)
op.add_column('devices', sa.Column('device_role', sa.String(), nullable=False))

Tablo Silme

  1. Önce kod referanslarını kaldır (import, query, relationship)
  2. Bir sonraki release'de migration ile tabloyu sil
  3. Asla aynı PR'da kod + tablo silme yapma

NOT VALID + VALIDATE (CHECK constraint genişletme)

Whitelist tipi CHECK constraint'ine yeni değer eklerken (örn. enum genişletme) tablo lock'ını minimize eden production-grade pattern:

# 1) Eski constraint DROP
op.execute("ALTER TABLE devices DROP CONSTRAINT ck_devices_chk_device_source")

# 2) Yeni constraint NOT VALID — anlık metadata, satır taraması YOK
op.execute(
"ALTER TABLE devices "
"ADD CONSTRAINT ck_devices_chk_device_source "
"CHECK (device_source IN ('modbus','zigbee','virtual','manual','ocpp')) "
"NOT VALID"
)

# 3) VALIDATE — ShareUpdateExclusiveLock; okuma/yazma trafiği bloklanmaz
op.execute("ALTER TABLE devices VALIDATE CONSTRAINT ck_devices_chk_device_source")

Lock matrisi:

AdımLock seviyesiSüreTrafik etkisi
DROP CONSTRAINTAccessExclusiveLockAnlıkMetadata — anlık
ADD ... NOT VALIDAccessExclusiveLockAnlıkMetadata — satır tarama YOK
VALIDATE CONSTRAINTShareUpdateExclusiveLockTablo boyutuna göreOkuma/yazma bloklanmaz

Pure DROP+ADD da hızlı ama büyüyen tablolarda full-scan AccessExclusiveLock'u uzar. NOT VALID + VALIDATE paterni Zeus 2.0 migration konvansiyonu (bkz. 0038_chk_device_source_ocpp.py).

Naming Convention

backend/app/core/database/base.py:11-19 SQLAlchemy MetaData üzerinde naming convention tanımlar:

convention = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
metadata = MetaData(naming_convention=convention)

Önemli: CheckConstraint(..., name="chk_device_source") ile model'de yazsan bile DB'deki gerçek ad ck_devices_chk_device_source olur ("ck": "ck_%(table_name)s_%(constraint_name)s" template uygulanır).

Raw SQL kullanırken bypass riski:

Migration'da op.execute("ALTER TABLE ... ADD CONSTRAINT chk_device_source CHECK ...") yazarsan convention bypass olur, sonradan ORM ile drop ederken ck_devices_chk_device_source aranır ve bulunmaz. Bunu doğrulamak için:

-- Tablodaki tüm constraint adlarını listele
SELECT conname, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'devices'::regclass;

Defansif drop pattern (0038 örneği): hem convention adı hem legacy ad listesi denenir.

Kurallar

  • Migration test edilmeden asla merge edilmez
  • TimescaleDB hypertable → op.execute() ile raw SQL
  • Büyük tablolarda index ekleme → CREATE INDEX CONCURRENTLY
  • Kolon rename → yeni kolon ekle, backfill, eski kolonu sil
  • CHECK constraint genişletme → NOT VALID + VALIDATE paterni
  • Naming convention'a sadık kal (raw SQL'de bypass riski)

Bilinen Tuzaklar

TuzakEtkiÇözüm
alembic_version.version_num VARCHAR(32)32 char üstü revision id StringDataRightTruncationErrorRevision id'yi kısa tut. alembic_version boyutunu büyütmek ayrı PR. Örnek: 0038_extend_chk_device_source_ocpp (34 char) FAIL → 0038_chk_device_source_ocpp (27 char) OK.
Naming convention bypassRaw SQL ile yaratılan constraint convention adıyla bulunmazYukarıdaki "Naming Convention" bölümü; defansif drop pattern.
Multiple headsalembic upgrade head belirsiz davranırMerge migration. migration-sanity CI job tek-head invariant'ını garanti eder.
TimescaleDB hypertableStandart Alembic create_table hypertable üretmezop.execute("SELECT create_hypertable('table', 'time')") raw SQL.
MIGRATION_TARGET overrideProduction drift riskiPR #277 sonrası varsayılan head. Override sadece staging hot-fix için. Detay: CI/CD Pipeline.

Multiple Heads ve Merge Migration (Issue #256)

Sorun

Birden fazla geliştirici aynı parent revision'a paralel olarak fork yapıp ayrı migration'lar oluşturduğunda Alembic multiple heads durumuna düşer:

$ alembic heads
0034_remove_sofar_power_from_metadata (head)
0035_add_mqtt_gateway_bus_unique (head)

Bu durumda alembic upgrade head komutu hangi head'e gideceği belirsiz olduğu için başarısız olur veya yanlış sıralı uygulamaya neden olur.

Çözüm: Merge Migration

alembic merge ile iki head birleştirilir. Sonuç olarak down_revision tuple haline gelir:

# 0036_merge_sofar_mqtt.py
"""merge sofar power columns + mqtt gateway bus unique"""

revision: str = "merge_sofar_mqtt"
down_revision: tuple = (
"sofar_power_columns", # 0034
"mqtt_gw_bus_unique", # 0035
)
branch_labels = None
depends_on = None


def upgrade() -> None:
pass # Merge-only, schema değişikliği yok


def downgrade() -> None:
pass

Komut

alembic merge -m "merge sofar mqtt" 0034_remove_sofar_power_from_metadata 0035_add_mqtt_gateway_bus_unique

Best Practice — head + single-head invariant (PR #277)

PR #277 sonrası MIGRATION_TARGET varsayılanı head'tir. Güvenliği iki katmanla sağlanır:

  1. migration-sanity CI job — multiple heads varsa CI fail eder, deploy hiç başlamaz.
  2. Merge migration konvansiyonu — paralel branch'lerden gelen iki head merge edilmeden head'e push'lanmaz.
# ✅ Doğru — varsayılan akış
MIGRATION_TARGET="${MIGRATION_TARGET:-head}"
alembic upgrade "${MIGRATION_TARGET}"

# ✅ Doğru — staging hot-fix için spesifik revision pin'i
MIGRATION_TARGET=0037_add_ocpp_tables alembic upgrade $MIGRATION_TARGET
Production'da gereksiz override yapma

Production'da MIGRATION_TARGET spesifik revision'a pin'lemek migration drift'in başlangıcıdır. Sadece staging'de hot-fix testi için kullanın. Detay: CI/CD Pipeline.

migration-sanity Job (CI)

CI pipeline'ında pre-deploy doğrulama olarak çalışır:

HEADS=$(alembic heads | wc -l)
if [ "$HEADS" -ne 1 ]; then
echo "ERROR: Multiple alembic heads detected — merge migration gerekli"
alembic heads
exit 1
fi

Bu job tek satır head garantisi sağlar; PR merge öncesi multiple-heads durumu yakalanır.