login

local ffi = require "ffi"

ffi.cdef[[
typedef struct sqlite3 sqlite3;
typedef struct sqlite3_stmt sqlite3_stmt;
int sqlite3_open(
  const char *filename,   /* Database filename (UTF-8) */
  sqlite3 **ppDb          /* OUT: SQLite db handle */
);
int sqlite3_close(sqlite3*);
int sqlite3_prepare_v2(sqlite3 *conn, const char *zSql, int nByte,
  sqlite3_stmt **ppStmt, const char **pzTail);
int sqlite3_bind_null(sqlite3_stmt*, int);
int sqlite3_bind_double(sqlite3_stmt*, int, double);
int sqlite3_bind_text(sqlite3_stmt*,int,const char*,int,void(*)(void*));
int sqlite3_step(sqlite3_stmt*);
int sqlite3_column_count(sqlite3_stmt *pStmt);
const char *sqlite3_column_name(sqlite3_stmt*, int N);
int sqlite3_column_type(sqlite3_stmt*, int iCol);
int sqlite3_column_int(sqlite3_stmt*, int iCol);
double sqlite3_column_double(sqlite3_stmt*, int iCol);
const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol);
const void *sqlite3_column_blob(sqlite3_stmt*, int iCol);
int sqlite3_reset(sqlite3_stmt *pStmt);
int sqlite3_finalize(sqlite3_stmt *pStmt);
]]
local C = ffi.load("sqlite3")

local CODE = {
    [0] = "OK", "ERROR", "INTERNAL", "PERM", "ABORT", "BUSY", "LOCKED", "NOMEM",
    "READONLY", "INTERRUPT", "IOERR", "CORRUPT", "NOTFOUND", "FULL", "CANTOPEN",
    "PROTOCOL", "EMPTY", "SCHEMA", "TOOBIG", "CONSTRAINT", "MISMATCH", "MISUSE",
    "NOLFS", "AUTH", "FORMAT", "RANGE", "NOTADB", "NOTICE", "WARNING",
    [100] = "ROW", [101] = "DONE"
}

local TYPE = {"INTEGER", "FLOAT", "TEXT", "BLOB", "NULL"}

local DB = {}
DB.__index = DB

function DB:execute(sql, ...)
    local pstmt = ffi.new("sqlite3_stmt *[1]")
    local res = CODE[C.sqlite3_prepare_v2(self.db, sql, #sql, pstmt, nil)]
    if res ~= "OK" then error(sql) end
    local stmt = pstmt[0]
    local arg = {...}
    for i, v in ipairs(arg) do
        if type(v) == "nil" then
            C.sqlite3_bind_null(stmt, i)
        elseif type(v) == "number" then
            C.sqlite3_bind_double(stmt, i, v)
        elseif type(v) == "string" then
            C.sqlite3_bind_text(stmt, i, v, #v, ffi.cast("void(*)(void*)", 0))
        else
            error(("invalid type for query parameter: %s"):format(type(v)))
        end
    end
    local rows = {}
    repeat
        local done = true
        local res = CODE[C.sqlite3_step(stmt)]
        -- TODO: handle res == "BUSY"
        if res == "ROW" then
            local row = {}
            local ncols = C.sqlite3_column_count(stmt)
            for i = 0, ncols-1 do
                local col_name = ffi.string(C.sqlite3_column_name(stmt, i))
                local col_type = TYPE[C.sqlite3_column_type(stmt, i)]
                local value
                if col_type == "INTEGER" then
                    value = C.sqlite3_column_int(stmt, i)
                elseif col_type == "FLOAT" then
                    value = C.sqlite3_column_double(stmt, i)
                elseif col_type == "TEXT" then
                    value = ffi.string(C.sqlite3_column_text(stmt, i))
                elseif col_type == "BLOB" then
                    value = C.sqlite3_column_blob(stmt, i)
                end
                row[col_name] = value
            end
            table.insert(rows, row)
            done = false
        end
    until done
    C.sqlite3_finalize(stmt)
    return rows
end

function DB:close()
    C.sqlite3_close(self.db)
end

local function open(fname)
    local self = setmetatable({}, DB)
    local pdb = ffi.new("sqlite3 *[1]")
    C.sqlite3_open(fname, pdb)
    self.db = pdb[0]
    self.err = ffi.new("char *[1]")
    return self
end

return {open=open}