Setiap commit pada table format menghasilkan satu snapshot yang tidak berubah. Properti sederhana ini berdampak besar: tabel tidak hanya menyimpan keadaan terbaru, tetapi seluruh riwayatnya. Query versi lampau — time travel — kemudian menjadi fitur bawaan, bukan tambahan. Chapter ini menguji mekanisme tersebut di Delta Lake dengan empat percobaan nyata: membuka kembali setiap versi, membaca sambil penulis lain bekerja (snapshot isolation), membersihkan berkas tidak terpakai (vacuum), dan memulihkan keadaan lama (rollback).

Tiga Penulisan, Tiga Keadaan #

Blok pertama melakukan tiga penulisan berturut-turut, lalu membuka ketiga versinya. Penulisan pertama juga menyetel delta.logRetentionDuration — konfigurasi berapa lama berkas log versi lama disimpan sebelum boleh dibersihkan:

python
import os
import shutil

import pyarrow as pa
from deltalake import DeltaTable, write_deltalake

BASE = "delta-versi/pesanan"
if os.path.exists(BASE):
    shutil.rmtree(BASE)

skema = pa.schema(
    [
        pa.field("id", pa.int64()),
        pa.field("nama", pa.string()),
        pa.field("jumlah", pa.int64()),
    ]
)


def tabel(rows):
    return pa.Table.from_arrays(
        [
            pa.array([r[0] for r in rows], pa.int64()),
            pa.array([r[1] for r in rows]),
            pa.array([r[2] for r in rows], pa.int64()),
        ],
        schema=skema,
    )


# tiga penulisan -> tiga versi; atur juga retensi log
konfigurasi = {"delta.logRetentionDuration": "interval 30 days"}
write_deltalake(BASE, tabel([(1, "Budi", 2), (2, "Siti", 1), (3, "Dewi", 5)]), configuration=konfigurasi)
write_deltalake(BASE, tabel([(4, "Ani", 3)]), mode="append")
write_deltalake(BASE, tabel([(5, "Rina", 2)]), mode="append")

for versi in range(3):
    dt_v = DeltaTable(BASE, version=versi)
    isi = dt_v.to_pyarrow_table()
    nama = [b["nama"] for b in sorted(isi.to_pylist(), key=lambda r: r["id"])]
    print(f"versi {versi}: {isi.num_rows} baris -> {', '.join(nama)}")

print("pertumbuhan baris: 3 -> 4 -> 5")

Output yang Dihasilkan:

text
versi 0: 3 baris -> Budi, Siti, Dewi
versi 1: 4 baris -> Budi, Siti, Dewi, Ani
versi 2: 5 baris -> Budi, Siti, Dewi, Ani, Rina
pertumbuhan baris: 3 -> 4 -> 5

Mari kita telaah baris-baris kode di atas:

  1. Baris 32 (konfigurasi): configuration menerima properti tabel Delta; interval 30 days adalah nilai bawaan protokol untuk retensi log, ditulis eksplisit agar mudah diubah saat bereksperimen.
  2. Baris 33-35 (tiga write_deltalake): tiga commit menghasilkan tiga berkas log — ...00000.json, ...00001.json, ...00002.json — dan tiga berkas Parquet.
  3. Baris 37-41 (for versi in range(3)): setiap iterasi membuat objek DeltaTable terikat pada satu versi, lalu membaca isi versi itu; hasilnya diurutkan berdasarkan id agar output stabil.

Snapshot Isolation #

Pembaca tidak terganggu oleh penulis baru. Objek DeltaTable menahan snapshot yang dibacanya sampai dipanggil update_incremental() atau dibuat ulang — perilaku yang dikenal sebagai snapshot isolation:

python
import pyarrow as pa
from deltalake import DeltaTable, write_deltalake

BASE = "delta-versi/pesanan"

skema = pa.schema(
    [
        pa.field("id", pa.int64()),
        pa.field("nama", pa.string()),
        pa.field("jumlah", pa.int64()),
    ]
)


def tabel(rows):
    return pa.Table.from_arrays(
        [
            pa.array([r[0] for r in rows], pa.int64()),
            pa.array([r[1] for r in rows]),
            pa.array([r[2] for r in rows], pa.int64()),
        ],
        schema=skema,
    )


# pembaca menahan snapshot versi 2
pembaca_lama = DeltaTable(BASE, version=2)
print("handle versi 2:", pembaca_lama.version(), "|", pembaca_lama.to_pyarrow_table().num_rows, "baris")

# penulis lain menambahkan versi 3
write_deltalake(BASE, tabel([(6, "Joko", 4)]), mode="append")

print("handle lama tetap:", pembaca_lama.version(), "|", pembaca_lama.to_pyarrow_table().num_rows, "baris")
pembaca_baru = DeltaTable(BASE)
print("handle baru:", pembaca_baru.version(), "|", pembaca_baru.to_pyarrow_table().num_rows, "baris")

Output yang Dihasilkan:

text
handle versi 2: 2 | 5 baris
handle lama tetap: 2 | 5 baris
handle baru: 3 | 6 baris

Laporan panjang yang dibaca selama sepuluh menit tetap konsisten dari awal sampai akhir meskipun penulis lain meng-commit di tengah proses — inilah dasar reproduksibilitas: hasil query terikat ke versi, bukan ke jam dinding.

Vacuum dan Retensi Versi #

Time travel memakan tempat: berkas Parquet versi lama tidak dihapus otomatis. Vacuum membersihkan berkas yang sudah tidak dirujuk versi mana pun, dengan dua pengaman: mode dry_run hanya mendaftar tanpa menghapus, dan durasi retensi melindungi berkas yang masih terlalu baru. Blok ini juga mendemonstrasikan restore() — memulihkan keadaan versi 3 setelah sebuah overwrite:

python
import pyarrow as pa
from deltalake import DeltaTable, write_deltalake

BASE = "delta-versi/pesanan"

skema = pa.schema(
    [
        pa.field("id", pa.int64()),
        pa.field("nama", pa.string()),
        pa.field("jumlah", pa.int64()),
    ]
)


def tabel(rows):
    return pa.Table.from_arrays(
        [
            pa.array([r[0] for r in rows], pa.int64()),
            pa.array([r[1] for r in rows]),
            pa.array([r[2] for r in rows], pa.int64()),
        ],
        schema=skema,
    )


# overwrite mengganti seluruh isi tabel dengan satu baris
write_deltalake(BASE, tabel([(7, "Tuti", 1)]), mode="overwrite")
dt = DeltaTable(BASE)
print("setelah overwrite: versi", dt.version(), "|", dt.to_pyarrow_table().num_rows, "baris")

# vacuum dry-run: daftar berkas yang akan dihapus, tanpa benar-benar menghapus
terlindung = dt.vacuum(dry_run=True)
paksa = dt.vacuum(retention_hours=0, dry_run=True, enforce_retention_duration=False)
print("vacuum dry-run (retensi bawaan):", len(terlindung), "berkas (semua masih terlalu baru)")
print("vacuum dry-run (retensi 0 jam):", len(paksa), "berkas parquet tidak terpakai")

# rollback: pulihkan keadaan versi 3
hasil_restore = dt.restore(target=3)
print("restore ke versi 3:", hasil_restore)

dt = DeltaTable(BASE)
isi = dt.to_pyarrow_table()
nama = [b["nama"] for b in sorted(isi.to_pylist(), key=lambda r: r["id"])]
print("setelah restore: versi", dt.version(), "|", isi.num_rows, "baris ->", ", ".join(nama))

print("konfigurasi retensi:", dt.metadata().configuration)

Output yang Dihasilkan:

text
setelah overwrite: versi 4 | 1 baris
vacuum dry-run (retensi bawaan): 0 berkas (semua masih terlalu baru)
vacuum dry-run (retensi 0 jam): 4 berkas parquet tidak terpakai
restore ke versi 3: {'numRemovedFile': 1, 'numRestoredFile': 4}
setelah restore: versi 5 | 6 baris -> Budi, Siti, Dewi, Ani, Rina, Joko
konfigurasi retensi: {'delta.logRetentionDuration': 'interval 30 days'}

Tiga angka pada output tersebut layak dibaca pelan. Pertama, vacuum dengan retensi bawaan mengembalikan nol berkas — protokol Delta melindungi berkas yang lebih muda dari tujuh hari, dan seluruh berkas kita baru berumur beberapa detik. Kedua, memaksa retensi 0 jam memperlihatkan empat berkas Parquet yang sudah tidak dirujuk versi mana pun setelah overwrite menghapus empat commit sebelumnya. Ketiga, restore menulis commit baru (versi 5) yang mengembalikan rujukan ke berkas versi 3 — rollback tidak menghapus riwayat, justru menambah satu entri lagi; baris numRestoredFile: 4 mencatat empat berkas yang dirujuk kembali. Nilai bawaan delta.logRetentionDuration adalah 30 hari dan delta.deletedFileRetentionDuration tujuh hari — keduanya bisa diubah per tabel seperti konfigurasi pada blok pertama; rincian protokolnya ada di dokumentasi Delta Lake dan dokumentasi delta-rs .

Use Case: Audit, Reproduksi, dan Rollback #

  • Audit — history() menyebut siapa, kapan, dan berapa baris per commit; versi menjadi bukti yang tidak bisa diringkas.
  • Debugging — ketika angka hari ini mencurigakan, bandingkan versi sebelum dan sesudah perubahan untuk menemukan commit yang mengubah hasil.
  • Reproduksi laporan — laporan tertanggal 15 bulan lalu diterbitkan ulang dengan query versi saat laporan itu dibuat, bukan versi hari ini.
  • Rollback — restore() membatalkan kesalahan penulisan tanpa menyalin data secara manual.

Semua use case tersebut berdiri di atas satu syarat: retensi. Simpan log lebih lama dari kebutuhan audit Anda, jadwalkan vacuum setelah periode retensi terlewati, dan catat nomor versi pada setiap artefak (laporan, model) yang diterbitkan.

Dengan transaksi, schema, dan versi yang lengkap, semua bahan arsitektur lakehouse sudah ada di meja. Tersisa satu pertanyaan penutup section ini: arsitektur mana yang paling cocok untuk konteks kita — pertanyaan yang dijawab pada Chapter Memilih Arsitektur Lakehouse.