Files
vfs/sqlite_test.go
469995ca0c fix(sqlite): use hanzoai/sqlite (the one driver), drop direct modernc import (#7)
* fix(sqlite): use hanzoai/sqlite (the one driver), drop direct modernc import

* fix(sqlite): use hanzoai/sqlite (the one driver), drop direct modernc import

---------

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-10 02:22:58 -07:00

208 lines
5.6 KiB
Go

package vfs_test
// SQLite roundtrip — proves a real SQLite database file written through
// VFS (open → INSERT → close) is byte-identical to one usable by SQLite
// after restore from VFS bytes. The path is:
//
// 1. Build a SQLite DB on a normal file (the "reference path") —
// sqlite3 driver opens, runs CREATE/INSERT, closes.
// 2. Read the DB bytes off disk, write them to a vfs.File at offset 0,
// Sync.
// 3. Open the VFS, ReadAt the entire file back into an in-memory
// buffer.
// 4. Hand the buffer to SQLite via the deserialize() API and run
// SELECT — values must match.
//
// What this proves:
// - byte-for-byte fidelity through encrypt → backend → decrypt across
// hundreds of 4 KiB pages
// - block-aligned writes + reads at non-zero offsets
// - file size tracking matches exact byte count, not block-rounded
// - the file produced by VFS is usable as a SQLite database
//
// What FUSE would add (post-0.2.0):
// - SQLite opens the file directly via the kernel VFS (no copy step)
// - fcntl locking semantics
// - mmap (optional in SQLite)
import (
"context"
"database/sql"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
_ "github.com/hanzoai/sqlite"
"github.com/luxfi/age"
"github.com/hanzoai/vfs"
"github.com/hanzoai/vfs/pkg/backend"
_ "github.com/hanzoai/vfs/pkg/backend/file"
)
func TestSQLiteRoundTrip(t *testing.T) {
tmp := t.TempDir()
refPath := filepath.Join(tmp, "ref.db")
// 1. Build a reference SQLite DB on a normal file.
{
db, err := sql.Open("sqlite", refPath)
if err != nil {
t.Fatalf("sql.Open ref: %v", err)
}
if _, err := db.Exec(`CREATE TABLE coins (sym TEXT PRIMARY KEY, name TEXT, decimals INT)`); err != nil {
t.Fatalf("CREATE: %v", err)
}
stmt, err := db.Prepare(`INSERT INTO coins (sym, name, decimals) VALUES (?, ?, ?)`)
if err != nil {
t.Fatalf("Prepare: %v", err)
}
rows := []struct {
sym, name string
dec int
}{
{"USDC", "USD Coin", 6},
{"USDT", "Tether", 6},
{"BTC", "Bitcoin", 8},
{"ETH", "Ethereum", 18},
}
for i := 0; i < 200; i++ {
r := rows[i%len(rows)]
if _, err := stmt.Exec(fmt.Sprintf("%s_%d", r.sym, i), r.name, r.dec); err != nil {
t.Fatalf("INSERT: %v", err)
}
}
_ = stmt.Close()
if err := db.Close(); err != nil {
t.Fatalf("Close ref: %v", err)
}
}
refBytes, err := os.ReadFile(refPath)
if err != nil {
t.Fatalf("read ref: %v", err)
}
if len(refBytes) < vfs.BlockSize {
t.Fatalf("ref DB too small: %d bytes", len(refBytes))
}
t.Logf("reference DB is %d bytes (%d blocks)", len(refBytes), (len(refBytes)+vfs.BlockSize-1)/vfs.BlockSize)
// 2. Write those bytes through VFS.
fs := newFS(t)
if _, err := fs.Create("/test.db", 0o644); err != nil {
t.Fatalf("Create: %v", err)
}
f, err := fs.Open(context.Background(), "/test.db")
if err != nil {
t.Fatalf("Open: %v", err)
}
if _, err := f.WriteAt(refBytes, 0); err != nil {
t.Fatalf("WriteAt: %v", err)
}
if err := f.Sync(); err != nil {
t.Fatalf("Sync: %v", err)
}
stat, _ := f.Stat()
if stat.Size != uint64(len(refBytes)) {
t.Fatalf("size mismatch: vfs=%d ref=%d", stat.Size, len(refBytes))
}
if err := f.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
// 3. Read them back from VFS.
f2, err := fs.Open(context.Background(), "/test.db")
if err != nil {
t.Fatalf("re-Open: %v", err)
}
buf := make([]byte, len(refBytes))
n, err := f2.ReadAt(buf, 0)
if err != nil && err != io.EOF {
t.Fatalf("ReadAt: %v", err)
}
if n != len(refBytes) {
t.Fatalf("ReadAt n=%d want %d", n, len(refBytes))
}
_ = f2.Close()
if string(buf) != string(refBytes) {
// Find first divergence to localise the bug
for i := range refBytes {
if buf[i] != refBytes[i] {
start := i - 16
if start < 0 {
start = 0
}
end := i + 16
if end > len(refBytes) {
end = len(refBytes)
}
t.Fatalf("byte mismatch at offset %d (block %d, off %d): vfs=% x ref=% x",
i, i/vfs.BlockSize, i%vfs.BlockSize, buf[start:end], refBytes[start:end])
}
}
t.Fatal("buffers differ but identical scan — this should not happen")
}
// 4. Hand the bytes to SQLite via a fresh on-disk file and verify
// the database is fully usable: SELECT count, integrity_check.
rebuiltPath := filepath.Join(tmp, "rebuilt.db")
if err := os.WriteFile(rebuiltPath, buf, 0o644); err != nil {
t.Fatalf("WriteFile rebuilt: %v", err)
}
db, err := sql.Open("sqlite", rebuiltPath)
if err != nil {
t.Fatalf("sql.Open rebuilt: %v", err)
}
defer db.Close()
var count int
if err := db.QueryRow(`SELECT count(*) FROM coins`).Scan(&count); err != nil {
t.Fatalf("SELECT count: %v", err)
}
if count != 200 {
t.Fatalf("count=%d want 200", count)
}
// SQLite's own DB-integrity check
var ok string
if err := db.QueryRow(`PRAGMA integrity_check`).Scan(&ok); err != nil {
t.Fatalf("integrity_check: %v", err)
}
if !strings.EqualFold(ok, "ok") {
t.Fatalf("integrity_check = %q want ok", ok)
}
t.Logf("SQLite integrity_check: %s (200 rows verified)", ok)
}
func newFSScoped(t *testing.T) *vfs.FS {
t.Helper()
dir := t.TempDir()
be, err := backend.Open(context.Background(), "file://"+dir)
if err != nil {
t.Fatalf("backend.Open: %v", err)
}
t.Cleanup(func() { _ = be.Close() })
id, err := age.GenerateX25519Identity()
if err != nil {
t.Fatalf("age: %v", err)
}
c, err := vfs.NewCrypto([]age.Recipient{id.Recipient()}, []age.Identity{id})
if err != nil {
t.Fatalf("NewCrypto: %v", err)
}
v, err := vfs.New(vfs.Config{Backend: be, Crypto: c})
if err != nil {
t.Fatalf("vfs.New: %v", err)
}
fs, err := vfs.NewFS(context.Background(), v)
if err != nil {
t.Fatalf("NewFS: %v", err)
}
return fs
}