|
/* |
|
* A real SQLite, compiled to WebAssembly, querying a database that it reads |
|
* through Nix's own host functions. |
|
* |
|
* There is no filesystem here. SQLITE_OS_OTHER=1 strips SQLite's usual VFS |
|
* layer out entirely, and we supply a read-only one whose xRead is a call |
|
* back into the Nix evaluator via `read_file_range`. SQLite's pager thinks it |
|
* is doing ordinary positional reads against a file; every one of them is |
|
* actually a `pread` performed by the host on our behalf. |
|
*/ |
|
|
|
#include "sqlite3.h" |
|
|
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <stdio.h> |
|
|
|
/* ------------------------------------------------------------------ * |
|
* Nix host interface. All of these are imported from the "env" module. |
|
* ------------------------------------------------------------------ */ |
|
|
|
#define NIX_IMPORT(name) \ |
|
__attribute__((import_module("env"), import_name(#name))) |
|
|
|
typedef unsigned ValueId; |
|
|
|
NIX_IMPORT(read_file) extern unsigned nix_read_file(ValueId path, void *ptr, unsigned len); |
|
NIX_IMPORT(read_file_range) extern unsigned nix_read_file_range(ValueId path, unsigned long long off, |
|
void *ptr, unsigned len); |
|
NIX_IMPORT(get_attr) extern ValueId nix_get_attr(ValueId set, const char *ptr, unsigned len); |
|
NIX_IMPORT(copy_string) extern unsigned nix_copy_string(ValueId v, char *ptr, unsigned max); |
|
NIX_IMPORT(make_int) extern ValueId nix_make_int(long long n); |
|
NIX_IMPORT(make_string) extern ValueId nix_make_string(const char *ptr, unsigned len); |
|
NIX_IMPORT(make_null) extern ValueId nix_make_null(void); |
|
NIX_IMPORT(make_attrset) extern ValueId nix_make_attrset(const void *ptr, unsigned len); |
|
NIX_IMPORT(make_list) extern ValueId nix_make_list(const void *ptr, unsigned len); |
|
NIX_IMPORT(make_float) extern ValueId nix_make_float(double x); |
|
NIX_IMPORT(panic) extern void nix_panic(const char *ptr, unsigned len); |
|
NIX_IMPORT(warn) extern void nix_warn(const char *ptr, unsigned len); |
|
NIX_IMPORT(return_to_nix) extern void nix_return(ValueId v); |
|
|
|
/* The attribute layout make_attrset expects: 12 bytes each. */ |
|
struct NixAttr { |
|
unsigned name_ptr; |
|
unsigned name_len; |
|
unsigned value_id; |
|
}; |
|
|
|
static void die(const char *msg) |
|
{ |
|
nix_panic(msg, strlen(msg)); |
|
__builtin_unreachable(); |
|
} |
|
|
|
/* ------------------------------------------------------------------ * |
|
* A read-only SQLite VFS backed by Nix's read_file_range. |
|
* ------------------------------------------------------------------ */ |
|
|
|
/* xOpen only receives a filename string, but what we actually need is the |
|
ValueId of the Nix path. Stash it here for the duration of the open. */ |
|
static ValueId g_pathId; |
|
|
|
typedef struct NixFile { |
|
sqlite3_file base; |
|
ValueId pathId; |
|
sqlite3_int64 size; |
|
} NixFile; |
|
|
|
static int nixClose(sqlite3_file *f) |
|
{ |
|
(void) f; |
|
return SQLITE_OK; |
|
} |
|
|
|
static int nixRead(sqlite3_file *f, void *buf, int amt, sqlite3_int64 off) |
|
{ |
|
NixFile *p = (NixFile *) f; |
|
|
|
/* The one line that matters: SQLite's pager asks for a page, and we ask |
|
the Nix evaluator for exactly those bytes. */ |
|
unsigned got = nix_read_file_range(p->pathId, (unsigned long long) off, buf, (unsigned) amt); |
|
|
|
if (got < (unsigned) amt) { |
|
memset((char *) buf + got, 0, (unsigned) amt - got); |
|
return SQLITE_IOERR_SHORT_READ; |
|
} |
|
return SQLITE_OK; |
|
} |
|
|
|
static int nixWrite(sqlite3_file *f, const void *b, int amt, sqlite3_int64 off) |
|
{ |
|
(void) f; (void) b; (void) amt; (void) off; |
|
return SQLITE_READONLY; |
|
} |
|
|
|
static int nixTruncate(sqlite3_file *f, sqlite3_int64 sz) { (void) f; (void) sz; return SQLITE_READONLY; } |
|
static int nixSync(sqlite3_file *f, int flags) { (void) f; (void) flags; return SQLITE_OK; } |
|
|
|
static int nixFileSize(sqlite3_file *f, sqlite3_int64 *pSize) |
|
{ |
|
*pSize = ((NixFile *) f)->size; |
|
return SQLITE_OK; |
|
} |
|
|
|
static int nixLock(sqlite3_file *f, int l) { (void) f; (void) l; return SQLITE_OK; } |
|
static int nixUnlock(sqlite3_file *f, int l) { (void) f; (void) l; return SQLITE_OK; } |
|
static int nixCheckReservedLock(sqlite3_file *f, int *pRes) { (void) f; *pRes = 0; return SQLITE_OK; } |
|
static int nixFileControl(sqlite3_file *f, int op, void *pArg) { (void) f; (void) op; (void) pArg; return SQLITE_NOTFOUND; } |
|
static int nixSectorSize(sqlite3_file *f) { (void) f; return 4096; } |
|
|
|
static int nixDeviceCharacteristics(sqlite3_file *f) |
|
{ |
|
(void) f; |
|
/* IMMUTABLE tells SQLite the file cannot change underneath it, so it |
|
skips locking and journal recovery entirely. */ |
|
return SQLITE_IOCAP_IMMUTABLE; |
|
} |
|
|
|
static const sqlite3_io_methods nixIoMethods = { |
|
.iVersion = 1, |
|
.xClose = nixClose, |
|
.xRead = nixRead, |
|
.xWrite = nixWrite, |
|
.xTruncate = nixTruncate, |
|
.xSync = nixSync, |
|
.xFileSize = nixFileSize, |
|
.xLock = nixLock, |
|
.xUnlock = nixUnlock, |
|
.xCheckReservedLock = nixCheckReservedLock, |
|
.xFileControl = nixFileControl, |
|
.xSectorSize = nixSectorSize, |
|
.xDeviceCharacteristics = nixDeviceCharacteristics, |
|
}; |
|
|
|
static int nixOpen(sqlite3_vfs *vfs, const char *name, sqlite3_file *f, int flags, int *pOut) |
|
{ |
|
(void) vfs; (void) name; |
|
NixFile *p = (NixFile *) f; |
|
|
|
memset(p, 0, sizeof(*p)); |
|
p->base.pMethods = &nixIoMethods; |
|
p->pathId = g_pathId; |
|
|
|
/* read_file with a zero-length buffer copies nothing and returns the |
|
size, which is exactly the stat() we do not otherwise have. */ |
|
p->size = nix_read_file(p->pathId, 0, 0); |
|
|
|
if (pOut) |
|
*pOut = flags & ~SQLITE_OPEN_READWRITE; |
|
return SQLITE_OK; |
|
} |
|
|
|
static int nixDelete(sqlite3_vfs *v, const char *n, int s) { (void) v; (void) n; (void) s; return SQLITE_OK; } |
|
|
|
static int nixAccess(sqlite3_vfs *v, const char *n, int flags, int *pRes) |
|
{ |
|
(void) v; (void) n; (void) flags; |
|
/* No journal, no WAL, no anything. The database is the only file. */ |
|
*pRes = 0; |
|
return SQLITE_OK; |
|
} |
|
|
|
static int nixFullPathname(sqlite3_vfs *v, const char *in, int n, char *out) |
|
{ |
|
(void) v; |
|
size_t len = strlen(in); |
|
if (len >= (size_t) n) |
|
len = (size_t) n - 1; |
|
memcpy(out, in, len); |
|
out[len] = 0; |
|
return SQLITE_OK; |
|
} |
|
|
|
static int nixRandomness(sqlite3_vfs *v, int n, char *out) { (void) v; memset(out, 0, n); return n; } |
|
static int nixSleep(sqlite3_vfs *v, int micro) { (void) v; (void) micro; return 0; } |
|
static int nixCurrentTime(sqlite3_vfs *v, double *p) { (void) v; *p = 2440587.5; return SQLITE_OK; } |
|
static int nixGetLastError(sqlite3_vfs *v, int n, char *p) { (void) v; (void) n; (void) p; return 0; } |
|
|
|
static sqlite3_vfs nixVfs = { |
|
.iVersion = 1, |
|
.szOsFile = sizeof(NixFile), |
|
.mxPathname = 1024, |
|
.zName = "nix", |
|
.xOpen = nixOpen, |
|
.xDelete = nixDelete, |
|
.xAccess = nixAccess, |
|
.xFullPathname = nixFullPathname, |
|
.xRandomness = nixRandomness, |
|
.xSleep = nixSleep, |
|
.xCurrentTime = nixCurrentTime, |
|
.xGetLastError = nixGetLastError, |
|
}; |
|
|
|
/* SQLITE_OS_OTHER=1 means SQLite has no built-in VFS, so it asks us. */ |
|
int sqlite3_os_init(void) { return sqlite3_vfs_register(&nixVfs, 1); } |
|
int sqlite3_os_end(void) { return SQLITE_OK; } |
|
|
|
/* ------------------------------------------------------------------ * |
|
* Entry point. |
|
* |
|
* Takes { db = <path>; sql = "<any SQL>"; } and returns a list of rows, |
|
* each row an attribute set keyed by column name. |
|
* ------------------------------------------------------------------ */ |
|
|
|
#define MAX_ROWS 65536 |
|
#define MAX_COLS 64 |
|
|
|
static ValueId g_rows[MAX_ROWS]; |
|
static struct NixAttr g_cols[MAX_COLS]; |
|
|
|
static ValueId getAttr(ValueId set, const char *name) |
|
{ |
|
ValueId v = nix_get_attr(set, name, strlen(name)); |
|
if (v == 0) |
|
die("argument must be an attrset with 'db' and 'sql'"); |
|
return v; |
|
} |
|
|
|
/* Copy a Nix string out of the evaluator into a fresh buffer. */ |
|
static char *dupNixString(ValueId v, unsigned *lenOut) |
|
{ |
|
unsigned len = nix_copy_string(v, 0, 0); |
|
char *buf = malloc(len + 1); |
|
if (!buf) |
|
die("out of memory"); |
|
nix_copy_string(v, buf, len); |
|
buf[len] = 0; |
|
if (lenOut) |
|
*lenOut = len; |
|
return buf; |
|
} |
|
|
|
int main(int argc, char **argv) |
|
{ |
|
if (argc < 2) |
|
die("expected the Nix ValueId as argv[1]"); |
|
|
|
ValueId arg = (ValueId) strtoul(argv[1], 0, 10); |
|
|
|
g_pathId = getAttr(arg, "db"); |
|
char *sql = dupNixString(getAttr(arg, "sql"), 0); |
|
|
|
sqlite3 *db = 0; |
|
if (sqlite3_open_v2("nixdb", &db, SQLITE_OPEN_READONLY, "nix") != SQLITE_OK) |
|
die("cannot open database"); |
|
|
|
sqlite3_stmt *stmt = 0; |
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, 0) != SQLITE_OK) |
|
die(sqlite3_errmsg(db)); |
|
|
|
int ncol = sqlite3_column_count(stmt); |
|
if (ncol > MAX_COLS) |
|
die("too many columns"); |
|
|
|
unsigned nrows = 0; |
|
int rc; |
|
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW && nrows < MAX_ROWS) { |
|
for (int i = 0; i < ncol; i++) { |
|
const char *name = sqlite3_column_name(stmt, i); |
|
ValueId value; |
|
|
|
switch (sqlite3_column_type(stmt, i)) { |
|
case SQLITE_INTEGER: |
|
value = nix_make_int(sqlite3_column_int64(stmt, i)); |
|
break; |
|
case SQLITE_FLOAT: |
|
value = nix_make_float(sqlite3_column_double(stmt, i)); |
|
break; |
|
case SQLITE_NULL: |
|
value = nix_make_null(); |
|
break; |
|
default: { |
|
const char *txt = (const char *) sqlite3_column_text(stmt, i); |
|
value = nix_make_string(txt, (unsigned) sqlite3_column_bytes(stmt, i)); |
|
break; |
|
} |
|
} |
|
|
|
g_cols[i].name_ptr = (unsigned) (unsigned long) name; |
|
g_cols[i].name_len = (unsigned) strlen(name); |
|
g_cols[i].value_id = value; |
|
} |
|
g_rows[nrows++] = nix_make_attrset(g_cols, (unsigned) ncol); |
|
} |
|
|
|
if (rc != SQLITE_DONE && rc != SQLITE_ROW) |
|
die(sqlite3_errmsg(db)); |
|
|
|
sqlite3_finalize(stmt); |
|
sqlite3_close(db); |
|
|
|
nix_return(nix_make_list(g_rows, nrows)); |
|
return 0; |
|
} |