Ba cách đưa SQLite vào Nix: Từ builtins.exec đến WebAssembly
Bài viết phân tích ba phương pháp tích hợp SQLite vào hệ thống Nix để truy vấn dữ liệu hiệu quả hơn so với JSON truyền thống, bao gồm builtins.exec, builtins.importNative và builtins.wasm. Mỗi cách đều có ưu nhược điểm riêng về hiệu năng và độ an toàn, mở ra hướng tiếp cận mới cho việc xử lý dữ liệu lớn trong hệ sinh thái Nix.

Ba cách đưa SQLite vào Nix: Từ builtins.exec đến WebAssembly
Khi dữ liệu index của dự án nixpkgs-multiverse ngày càng phình to, việc dùng JSON thuần túy với builtins.fromJSON trở nên kém hiệu quả vì phải parse toàn bộ file dù chỉ truy vấn một giá trị. Bài viết này khám phá ba phương án thay thế — từ builtins.exec, builtins.importNative đến builtins.wasm — giúp nhúng SQLite vào quy trình đánh giá của Nix, mỗi cách đều có đánh đổi riêng về hiệu năng và an toàn.
Vấn đề: JSON quá lớn, truy vấn quá chậm
Trong dự án nixpkgs-multiverse, index là một file JSON chứa 305.492 phiên bản gói phần mềm, trải dài qua 31.904 gói và 1.534 bản revision. Kích thước file versions.json hiện là 5,3 MB và history.json là 7,5 MB.
Vấn đề lớn nhất nằm ở builtins.fromJSON: nó được đánh giá một cách tham lam (eager), không có cơ chế parse lười (lazy) hay đọc streaming. Điều này có nghĩa là dù bạn chỉ cần thông tin của một gói duy nhất, Nix vẫn phải parse toàn bộ 5,3 MB và khởi tạo toàn bộ 305.492 giá trị trên heap.
"Nix attribute sets là một mảng đã được sắp xếp, nên việc tra cứu không phải là vấn đề. Chi phí nằm ở việc parse JSON và cấp phát bộ nhớ cho tất cả giá trị."
SQLite là giải pháp tự nhiên — chỉ tốn 0,01 giây và 4 MB để trả lời câu hỏi "gói hello có những phiên bản nào". Nhưng Nix không có builtins.sqlite, vậy phải làm sao?
Cách 1: builtins.exec — Lối thoát tối thượng
builtins.exec là builtin có từ tháng 4/2017, cho phép chạy một chương trình bên ngoài và parse stdout của nó như một biểu thức Nix. Tuy nhiên, nó bị khóa sau cờ allow-unsafe-native-code-during-evaluation.
let
versionsOf = attr: builtins.exec [
"${sqlite}/bin/sqlite3" "-noheader" "-separator" "" "./index.db"
''
SELECT '{' || group_concat(
'"' || version || '" = ' ||
COALESCE(CAST(rev AS TEXT), 'null') || ';', ' ')
|| '}'
FROM versions WHERE attr = '${attr}';
''
];
in
versionsOf "hello"
Điểm hay của cách này là SQLite có thể xuất trực tiếp cú pháp Nix, không cần định dạng trung gian. Tuy nhiên, mỗi truy vấn là một lần fork, exec, và parse lại output — chi phí khoảng 3,8 ms mỗi lần. Nếu bạn chỉ cần vài chục truy vấn, phương án này chấp nhận được nhờ sự đơn giản.
Cách 2: builtins.importNative — Nhúng C++ trực tiếp
Ra đời từ tháng 12/2014, builtins.importNative cho phép tải một thư viện chia sẻ (shared object) và gọi một hàm C++ với signature void (*)(EvalState &, Value &). Điểm mạnh là bạn có thể dùng Nix API để xây dựng attribute set trực tiếp, không qua bất kỳ dạng text trung gian nào.
void versions(EvalState & state, const PosIdx pos, Value ** args, Value & v)
{
// cache sqlite3 handles để tránh chi phí khởi tạo
auto * db = openOnce(state, pos, path);
sqlite3_prepare_v2(db, "SELECT version, rev FROM versions WHERE attr = ?1", ...);
// ... collect rows ...
auto bindings = state.buildBindings(rows.size());
for (auto & [version, rev] : rows) {
auto & slot = bindings.alloc(state.symbols.create(version));
if (rev) slot.mkInt(*rev); else slot.mkNull();
}
v.mkAttrs(bindings);
}
Với cách này, database handle được giữ qua nhiều lần gọi, nên các trang b-tree vẫn nằm trong bộ nhớ cache — tốc độ gần như không đổi, khoảng 0,05 giây cho toàn bộ dãy truy vấn thử nghiệm. Nhược điểm lớn nhất là yêu cầu cờ unsafe, khiến nó không phù hợp cho các dự án phân phối công khai.
Cách 3: builtins.wasm — Tương lai an toàn
Determinate Systems giới thiệu builtins.wasm vào tháng 3/2026, cho phép gọi hàm bên trong module WebAssembly. Đây là lựa chọn an toàn và xác định (deterministic) — khác hẳn hai builtin unsafe ở trên.
#[unsafe(no_mangle)]
pub extern "C" fn fib_entry(arg: ValueId) -> ValueId {
unsafe { make_int(fib(get_int(arg))) }
}
Điều thú vị là SQLite có bản build WASM chính thức. Tác giả đã phải vá thêm hàm read_file_range vào Nix API để đọc dữ liệu nhị phân từ disk (Nix strings không chứa được byte NULL), sau đó xây dựng một VFS tùy chỉnh cho SQLite:
static int nixRead(sqlite3_file *f, void *buf, int amt, sqlite3_int64 off)
{
// SQLite pager hỏi một trang, ta hỏi Nix evaluator đúng số byte đó
unsigned got = nix_read_file_range(p->pathId, off, buf, amt);
return got == amt ? SQLITE_OK : SQLITE_IOERR_SHORT_READ;
}
Kết quả là một SQLite đầy đủ chức năng — query planner, aggregate, subquery — chạy bên trong Nix evaluator qua WebAssembly. Mọi câu trả lời đều giống hệt CLI sqlite3.
So sánh hiệu năng: Ai thắng?
Kết quả benchmark với cùng câu hỏi "gói nào được ship ở revision nào":
| Phương pháp | Chi phí cố định | Chi phí/truy vấn | Ghi chú |
|---|---|---|---|
fromJSON | 0,29 giây | 0 | Flat line — parse một lần là xong |
File .nix khổng lồ | 0,53 giây | 0 | Tệ hơn JSON dù có lazy — parser Nix đắt hơn |
builtins.exec | ~0 | 3,8 ms | Vượt fromJSON sau ~80 truy vấn |
builtins.importNative | ~0 | ~0 | Tốt nhất — cache SQLite handle |
builtins.wasm | 2,5 giây | 7 ms | Chi phí JIT compile 1,1 MB SQLite |
Một phát hiện thú vị: file .nix khổng lồ — tưởng chừng nhờ lazy evaluation sẽ thắng — lại thua JSON. Lý do là Nix vẫn phải parse toàn bộ biểu thức qua Bison grammar, xây dựng AST cho 305.492 mục, và thêm mọi tên attribute vào symbol table. fromJSON bỏ qua AST, đi thẳng từ bytes đến values.
Kết luận: Chưa có "viên đạn bạc"
Với một lock file pin 30 gói, fromJSON vẫn thắng tuyệt đối ở kích thước index hiện tại. Nhưng khi dữ liệu phình to và nhu cầu truy vấn phức tạp tăng, các phương án trên mở ra hướng đi mới. Hiện tại, tác giả giữ JSON làm định dạng chính thức vì không muốn bắt người dùng bật cờ unsafe.
Tuy nhiên, tiềm năng của WASM là rõ ràng — đặc biệt khi Eelco (cha đẻ Nix) đã đề cập đến việc cache code đã biên dịch để tránh 2,5 giây JIT khởi động. Với hệ sinh thái đang phát triển, việc đưa SQLite chạy native trong Nix có thể không còn xa.
Hình minh họa cho bài viết
Bạn đang gặp vấn đề tương tự với dữ liệu lớn trong Nix? Hãy chia sẻ trải nghiệm của mình trong phần bình luận bên dưới nhé!