* 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>
208 lines
5.6 KiB
Go
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
|
|
}
|