SQLite Juga Membutuhkan Edition ala Rust
(mort.coffee)- SQLite adalah RDBMS mandiri yang banyak digunakan untuk penyimpanan lokal dan proyek embedded, tetapi karena kompatibilitas mundur, ia tetap mempertahankan default yang merugikan keamanan dan performa
- Secara default, SQLite tidak menegakkan constraint foreign key dan dapat menggunakan ulang
ROWID, sehingga bisa muncul masalah integritas data, misalnya postingan pengguna yang sudah dihapus terhubung ke pengguna lain yang mendapat ID yang sama - Tabel biasa juga dapat menyimpan tipe data yang berbeda dari kolom yang dideklarasikan; tabel STRICT mencegah hal ini, tetapi
strictharus ditentukan per tabel dan berbenturan dengan cara penggunaan nama tipe kustom SQLITE_BUSYyang muncul langsung pada penulisan bersamaan, WAL yang dinonaktifkan, dan pengaturan sinkronisasi yang konservatif masing-masing dapat diperbaiki dengan pragmabusy_timeout,journal_mode, dansynchronous- Dengan edition berbasis tahun seperti
PRAGMA edition = 2026, SQLite dapat memilih satu paket default yang aman, sehingga pengaturan default SQLite bisa terus berkembang tanpa merusak perilaku lama
Kekuatan SQLite dan default yang usang
- SQLite bisa disebut sebagai database untuk proyek embedded sekaligus standar industri untuk penyimpanan data lokal, dan beberapa perangkat lunak server seperti lobste.rs juga berjalan di SQLite
- Berbeda dari RDBMS tradisional yang berjalan sebagai proses terpisah, SQLite adalah RDBMS berbentuk library, sehingga perangkat lunak dapat tetap mandiri
- Karena tidak perlu menulis serializer dan parser kustom seperti pada format file biasa, SQLite memberikan kelebihan RDBMS sekaligus penyimpanan berbasis file
- Namun masih ada masalah bahwa beberapa default tidak tepat memengaruhi integritas data, konkurensi, dan performa
Masalah integritas akibat foreign key tidak ditegakkan
- Foreign key adalah sarana utama untuk menjaga konsistensi database dan mencegah referensi terputus, tetapi SQLite secara default tidak menegakkannya
- Bahkan jika
posts.user_iddideklarasikan merujuk keusers.id, pengaturan default tidak mencegah perilaku berikut- Membuat postingan tanpa ID pengguna yang valid
- Menghapus pengguna sambil membiarkan postingan milik pengguna tersebut tetap ada
INTEGER PRIMARY KEYmenjadi alias untukROWIDtabel, danalgoritme alokasi ROWIDSQLite menggunakan ulang ID dalam beberapa situasi- Jika Bob menulis postingan dengan ID
1lalu hanya akunnya yang dihapus, tidak ada error foreign key yang terjadi - Jika kemudian Alice mendapat ID
1yang digunakan ulang, postingan lama Bob akan terhubung ke Alice - Query-nya sendiri berhasil secara normal, sehingga masalah ini lebih sulit ditemukan daripada sekadar referensi yang terputus
- Jika Bob menulis postingan dengan ID
- Foreign key dapat ditegakkan dengan mengaktifkan pragma berikut pada setiap koneksi
PRAGMA foreign_keys = ON;
- Jika ini diterapkan sejak awal, penghapusan pengguna yang masih memiliki postingan akan dihentikan dengan error
FOREIGN KEY constraint failed
Tabel biasa mengizinkan tipe data yang berbeda dari deklarasi
- Nilai SQLite adalah salah satu dari
NULL,INTEGER,REAL,TEXT, atauBLOB, tetapi deklarasi kolom pada tabel biasa tidak membatasi tipe data yang dapat disimpan secara ketat; deklarasi itu menentukan type affinity - Untuk kolom dengan affinity
INTEGER, aturan penyimpanan berikut berlakuTEXTyang dapat diinterpretasikan sebagai bilangan bulat dikonversi menjadiINTEGERTEXTyang dapat diinterpretasikan sebagai bilangan real dikonversi menjadiREAL, yaitu floating-point presisi ganda- Nilai lainnya disimpan apa adanya dengan tipe data aslinya
- Affinity lain juga memiliki aturan konversi tersendiri
- Affinity
BLOBmenyimpan nilai apa adanya - Affinity
TEXTmembiarkanBLOB,TEXT, danNULLapa adanya, serta mengonversi angka menjadiTEXT - Affinity
REALbertindak seperti affinityINTEGER, tetapi mengonversi bilangan bulat menjadiREAL
- Affinity
- Karena itu, bahkan kolom
duration_sec INTEGERdapat menyimpan string nonangka seperti'Way too long, I mean come on' - Dalam proyek nyata, ada juga kasus ketika string
'1'dan'0'tercatat alih-alih angka1dan0untuk boolean, sehingga data harus dibersihkan dan di-debug
Tabel STRICT dan benturan dengan tipe kustom
- Tabel STRICT memunculkan error tipe seperti
cannot store TEXT value in INTEGER columnjika tipe data yang salah dimasukkan ke kolom
CREATE TABLE music (
id INTEGER PRIMARY KEY,
name TEXT,
duration_sec INTEGER
) strict;
-
Tidak ada pragma yang membuat semua tabel menjadi strict secara global, sehingga
strictharus ditambahkan tanpa terlewat pada setiap pernyataanCREATE TABLE -
SQLite berpendapat bahwa tipe yang fleksibel itu berguna, tetapi bahkan pada tabel STRICT, kita dapat membuat kolom yang menyimpan semua jenis nilai dengan menyatakan tipe data
ANY- Kolom yang mengizinkan tipe data arbitrer dan kolom yang memaksa tipe data tertentu dapat dibedakan secara eksplisit
-
Masalah kompatibilitas yang lebih realistis adalah bahwa tabel STRICT tidak hanya menegakkan tipe data, tetapi juga mengubah aturan interpretasi type specifier
-
Cara tabel biasa menginterpretasikan nama tipe
- Tabel SQLite biasa menentukan affinity berdasarkan string tipe yang dideklarasikan
- Jika mengandung
"INT", diperlakukan sebagaiINTEGER - Jika mengandung salah satu dari
"CHAR","CLOB", atau"TEXT", menjadiTEXT - Jika mengandung
"BLOB"atau tipenya dihilangkan, diperlakukan sebagaiBLOB - Jika mengandung salah satu dari
"REAL","FLOA", atau"DOUB", menjadiREAL - Jika tidak memenuhi kondisi mana pun, diperlakukan sebagai
NUMERIC - Jika tipe longgar digabungkan dengan aturan ini, nama tipe kustom seperti
DATETIME,KEY_VALUE_SET, atauCOLORdapat ditempelkan pada kolom - Konektor atau wrapper database dapat melihat nama tipe tersebut lalu otomatis melakukan serialisasi dan deserialisasi
- Bahkan tanpa fitur penanganan khusus, nama tipe itu sendiri mendokumentasikan data yang diharapkan pada kolom
- Jika STRICT begitu saja dijadikan default, pola penggunaan ini harus ditinggalkan, sehingga diperlukan alias tipe kustom yang eksplisit
CREATE TYPE KEY_VALUE_SET = TEXT;- Jika alias seperti
KEY_VALUE_SETdapat digunakan dalam tabel strict, pola data yang diharapkan dari kolomTEXTyang harus di-parse oleh aplikasi juga dapat dicatat di skema - Fitur untuk menghubungkan
constraint CHECKke tipe kustom juga akan berguna - Standar SQL:1999 sudah memiliki
CREATE DOMAIN, yaitu sintaks alias tipe yang mendukung hingga constraint, sehingga SQLite dapat mengimplementasikannya dengan mendukung pernyataan standar ini
Penulisan bersamaan dan SQLITE_BUSY
- SQLite mengizinkan banyak pembacaan bersamaan, tetapi penulisan hanya diproses satu per satu
- Dengan pengaturan default, ketika dua proses mencoba memperoleh lock tulis secara bersamaan, salah satunya langsung menerima error
SQLITE_BUSYtanpa menunggu- Karena penulisan yang melakukan I/O disk memang bisa melambat, lebih wajar jika menunggu lock dilepas selama beberapa waktu
- Ada juga kasus ketika sistem nyata berhenti sesekali sehingga loop retry harus ditulis sendiri
- Pengaturan berikut akan mencoba kembali memperoleh lock selama maksimal 5 detik, dan baru setelah itu mengembalikan
SQLITE_BUSY
PRAGMA busy_timeout = 5000;
- Dalam operasi normal, sebaiknya semua penulisan dikumpulkan ke satu proses, dan jika memungkinkan ke satu thread; penulisan bersamaan memang tidak bisa menjadi cepat
- Namun ada juga kasus operasi normal yang membutuhkan penulisan bersamaan
- Membersihkan database secara interaktif dengan tool command-line
sqlite3 - Menjalankan skrip untuk tugas administrasi yang jarang dilakukan dan tidak memiliki frontend terpisah
- Membersihkan database secara interaktif dengan tool command-line
- Karena pengaturan default, pekerjaan seperti ini dapat memicu
SQLITE_BUSYyang tidak terduga dan menghentikan perangkat lunak yang sedang berjalan
WAL dan default performa
- Jika dikonfigurasi dengan benar, SQLite bisa cukup cepat untuk menjalankan sebagian peran yang biasanya ditangani server besar seperti PostgreSQL atau MySQL, tetapi performa default-nya kurang baik
- Detail tuning untuk lingkungan server dapat dilihat lebih lanjut di Optimizing SQLite for servers
- Masalah default yang paling penting adalah Write-Ahead Log, yaitu WAL dinonaktifkan
PRAGMA journal_mode = WAL;
- WAL meningkatkan kecepatan tulis secara signifikan dalam sebagian besar situasi
- Jika digunakan bersama pengaturan berikut, jumlah sinkronisasi disk dapat dikurangi secara besar tanpa risiko korupsi data
PRAGMA synchronous = NORMAL;
- Perilaku spesifik
synchronousdijelaskan dalam dokumentasi pragma SQLite
Edition SQLite berbasis tahun
- Jika default lama diubah langsung sekarang, perangkat lunak lama bisa rusak dan pengguna dapat menjadi takut untuk meng-upgrade SQLite di masa depan
- Untuk memilih default yang lebih baik sambil mempertahankan kompatibilitas mundur, satu super pragma dapat ditambahkan
PRAGMA edition = 2026;
- Edition
2026setidaknya bertindak sebagai alias untuk pengaturan berikut
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
- Dalam edition ini, mode
strictjuga diterapkan sebagai default untuk tabel baru - Seperti Rust editions, ini adalah cara untuk secara eksplisit memilih perilaku default baru sambil mempertahankan perilaku kode lama
- Berbeda dari satu mode tunggal seperti
"use strict";di JavaScript, edition berbasis tahun dapat mengubah paket default yang masuk akal seiring zaman - Misalnya, jika WAL2 milik Hctree masuk ke branch utama pada 2034,
PRAGMA edition = 2034di masa depan dapat dikonfigurasi untuk memilihjournal_mode = WAL2 - Sistem edition dapat menjadi kompromi untuk mengembangkan default keamanan dan performa SQLite tanpa merusak perilaku default program lama dan tanpa membuat SQLite terus terikat pada pengaturan masa lalu
2 komentar
Secara teori ini terlihat bagus, tetapi kalau mempertimbangkan benturan kompatibilitas dan overhead, rasanya malah akan merugikan.
SQLite adalah DBMS yang lahir untuk berjalan di lingkungan embedded seperti eGIS,
di dunia embedded, hal seperti asal cast
void**lalu menimpa fungsi setelah deklarasi__weak__itu hal yang biasa. Di dunia seperti itu, apakah benar perlu sejauh itu? Saya jadi ragu.Pendapat Lobste.rs
Kemarin saya melihat kabar bahwa lobste.rs beralih ke SQLite (https://lobste.rs/s/ko1ji1/lobste_rs_is_now_running_on_sqlite), lalu menulis postingan ini
Saat membaca percakapan waktu itu, saya teringat ketidaknyamanan kecil pada nilai bawaan SQLite dan merangkumnya dalam sebuah tulisan blog
Kalau ada kesempatan, tulisan ini juga layak dibagikan di forum SQLite: https://sqlite.org/forum/forum
Dalam SQL,
CREATE TYPEdigunakan untuk membuat tipe komposit, dan Postgres mendukung range, enum, array, hingga tipe dasar yang cukup kompleksFitur yang diinginkan sebenarnya lebih dekat ke
CREATE DOMAIN. Kita bisa membuat newtype dari tipe konkret dasar sekaligus menetapkan nilai bawaan dan batasan, sehingga skema yang mengulang batasanCHECKyang sama di banyak tabel menjadi jauh lebih ringkas. Misalnya bisa ditulis seperticreate domain mything as text;ataucreate domain mything as integer not null check (VALUE < 5);Perlu juga dibahas jebakan yang cukup berbahaya bahwa
busy_timeoutsaja tidak bisa mencegah errorSQLITE_BUSYyang muncul seketikaJika transaksi dimulai dengan kueri hanya-baca lalu menjalankan kueri tulis untuk menaikkan lock yang sedang dipegang, ketika ada penulisan lain maka ia akan langsung gagal tanpa memedulikan pengaturan
busy_timeout. Solusinya adalah memakaibegin immediatePerlu mempertimbangkan bahwa sebagian pengaturan yang diikat ke edition yang diusulkan adalah properti koneksi dalam model SQLite, sementara sisanya adalah properti file database atau tabel individual
PRAGMA journal_mode = WALtetap tersimpan pada file database, tetapiPRAGMA foreign_keys = ONharus diatur ulang pada setiap koneksi.STRICTadalah properti per tabel, sehingga dalam satu database dimungkinkan mencampur tabel yang strict dan yang tidak. Jadi perlu diputuskan apakah PRAGMA terpadu ini akan didefinisikan sebagai properti file database atau koneksi. Saya juga penasaran apakah ada mekanisme bawaan yang memberi peringatan agar pustaka versi lama tidak merusak file dengan mengubah fitur yang tidak dikenalnyaPendekatan edition pada dasarnya tampak sebagai kompromi yang baik antara tujuan tim pengembang SQLite untuk mempertahankan kompatibilitas ke belakang dan kebutuhan pengguna yang menginginkan nilai bawaan modern yang tidak terlalu membingungkan
Belakangan ini saya sangat tertarik pada RocksDB. Seperti SQLite, ini adalah pustaka database embedded, tetapi justru saya melihat tidak adanya SQL sebagai kelebihan besar
Saya merasa keharusan mengekspresikan semuanya dalam SQL hanya menghambat saat bekerja dengan database
Sulit membayangkan bagaimana sistem yang memakai SQL seperti lobste.rs bisa dipindahkan ke RocksDB atau database key-value lainnya
Saya tidak setuju bahwa menetapkan
busy_timeoutadalah solusinya. SQLite sangat kuat performanya bila menggunakan satu thread penulis lalu melakukan batch pada transaksi logis yang saling tidak terkaitCara konkretnya dijelaskan di https://andersmurphy.com/2025/12/…
Namun juga umum untuk mengubah database secara langsung demi pekerjaan pembersihan, atau menjalankan tugas administrasi sesekali lewat skrip tanpa perlu membuat layar admin terpisah. Dalam situasi seperti itu, tidak baik jika server atau aplikasi bisa berhenti secara acak hanya karena waktu eksekusinya kebetulan bertabrakan