--[[ Does most of the database interaction. Notably, holds a connection to the open sqlite3 database in .conn ]] local sql = require("lsqlite3") local queries = require("queries") local config = require("config") local db = {} --[[ Runs an sql query and receives the 3 arguments back, prints a nice error message on fail, and returns true on success. ]] function db.sqlassert(r, errcode, err) if not r then if err then error(string.format("%d: %s",errcode, err)) elseif errcode then error(string.format("%d: %s",errcode, db.conn:errmsg())) end end return r end --[[ Continuously tries to perform an sql statement until it goes through ]] function db.do_sql(stmnt) if not stmnt then error("No statement",2) end local err local i = 0 repeat err = stmnt:step() if err == sql.BUSY then i = i + 1 coroutine.yield() end until(err ~= sql.BUSY or i > 10) assert(i < 10, "Database busy") return err end --[[ Provides an iterator that loops over results in an sql statement or throws an error, then resets the statement after the loop is done. ]] function db.sql_rows(stmnt) if not stmnt then error("No statement",2) end local err return function() err = stmnt:step() if err == sql.BUSY then coroutine.yield() elseif err == sql.ROW then return unpack(stmnt:get_values()) elseif err == sql.DONE then stmnt:reset() return nil else stmnt:reset() local msg = string.format( "SQL Iteration failed: %s : %s\n%s", tostring(err), db.conn:errmsg(), debug.traceback() ) log(LOG_CRIT,msg) error(msg) end end end --[[ Binds an argument to as statement with nice error reporting on failure stmnt :: sql.stmnt - the prepared sql statemnet call :: string - a string "bind" or "bind_blob" position :: number - the argument position to bind to data :: string - The data to bind ]] function db.sqlbind(stmnt,call,position,data) assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call) local f = stmnt[call](stmnt,position,data) if f ~= sql.OK then error(string.format("Failed call %s(%d,%q): %s", call, position, data, db.conn:errmsg()),2) end end local oldconfigure = configure db.conn = db.sqlassert(sql.open(config.db)) function configure(...) --Create sql tables assert(db.conn:exec(queries.create_table_authors)) --Create a fake "anonymous" user, so we don't run into trouble --so that no one runs into trouble being able to paste under this account. assert(db.conn:exec(queries.insert_anon_author)) --If/when an author deletes their account, all posts --and comments by that author are also deleted (on --delete cascade) this is intentional. This also --means that all comments by other users on a post --an author makes will also be deleted. -- --Post text uses zlib compression assert(db.conn:exec(queries.create_table_posts)) --Store the raw text so people can download it later, maybe --we can use it for "download as image" or "download as pdf" --in the future too. Stil stored zlib compressed assert(db.conn:exec(queries.create_table_raw_text)) --Maybe we want to store images one day? assert(db.conn:exec(queries.create_table_images)) --Comments on a post assert(db.conn:exec(queries.create_table_comments)) --Tags for a post assert(db.conn:exec(queries.create_table_tags)) --Index for tags assert(db.conn:exec(queries.create_index_tags)) --Store a cookie for logged in users. Logged in users can edit --their own posts, and edit their biographies. assert(db.conn:exec(queries.create_table_session)) return oldconfigure(...) end configure() function db.close() db.conn:close() end return db