| 1 | --[[ |
| 2 | |
| 3 | DB Tables: |
| 4 | Prosody - key-value, map |
| 5 | | host | user | store | key | type | value | |
| 6 | ProsodyArchive - list |
| 7 | | host | user | store | key | time | stanzatype | jsonvalue | |
| 8 | |
| 9 | Mapping: |
| 10 | Roster - Prosody |
| 11 | | host | user | "roster" | "contactjid" | type | value | |
| 12 | | host | user | "roster" | NULL | "json" | roster[false] data | |
| 13 | Account - Prosody |
| 14 | | host | user | "accounts" | "username" | type | value | |
| 15 | |
| 16 | Offline - ProsodyArchive |
| 17 | | host | user | "offline" | "contactjid" | time | "message" | json|XML | |
| 18 | |
| 19 | ]] |
| 20 | |
| 21 | local type = type; |
| 22 | local tostring = tostring; |
| 23 | local tonumber = tonumber; |
| 24 | local pairs = pairs; |
| 25 | local next = next; |
| 26 | local setmetatable = setmetatable; |
| 27 | local json = require "util.json"; |
| 28 | |
| 29 | local connection = ...; |
| 30 | local host,user,store = module.host; |
| 31 | local params = module:get_option("sql"); |
| 32 | |
| 33 | do -- process options to get a db connection |
| 34 | local DBI = require "DBI"; |
| 35 | |
| 36 | params = params or { driver = "SQLite3", database = "prosody.sqlite" }; |
| 37 | assert(params.driver and params.database, "invalid params"); |
| 38 | |
| 39 | prosody.unlock_globals(); |
| 40 | local dbh, err = DBI.Connect( |
| 41 | params.driver, params.database, |
| 42 | params.username, params.password, |
| 43 | params.host, params.port |
| 44 | ); |
| 45 | prosody.lock_globals(); |
| 46 | assert(dbh, err); |
| 47 | |
| 48 | dbh:autocommit(false); -- don't commit automatically |
| 49 | connection = dbh; |
| 50 | |
| 51 | if params.driver == "SQLite3" then -- auto initialize |
| 52 | local stmt = assert(connection:prepare("SELECT COUNT(*) FROM `sqlite_master` WHERE `type`='table' AND `name`='Prosody';")); |
| 53 | local ok = assert(stmt:execute()); |
| 54 | local count = stmt:fetch()[1]; |
| 55 | if count == 0 then |
| 56 | local stmt = assert(connection:prepare("CREATE TABLE `Prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);")); |
| 57 | assert(stmt:execute()); |
| 58 | module:log("debug", "Initialized new SQLite3 database"); |
| 59 | end |
| 60 | assert(connection:commit()); |
| 61 | --print("===", json.encode()) |
| 62 | end |
| 63 | end |
| 64 | |
| 65 | local function serialize(value) |
| 66 | local t = type(value); |
| 67 | if t == "string" or t == "boolean" or t == "number" then |
| 68 | return t, tostring(value); |
| 69 | elseif t == "table" then |
| 70 | local value,err = json.encode(value); |
| 71 | if value then return "json", value; end |
| 72 | return nil, err; |
| 73 | end |
| 74 | return nil, "Unhandled value type: "..t; |
| 75 | end |
| 76 | local function deserialize(t, value) |
| 77 | if t == "string" then return value; |
| 78 | elseif t == "boolean" then |
| 79 | if value == "true" then return true; |
| 80 | elseif value == "false" then return false; end |
| 81 | elseif t == "number" then return tonumber(value); |
| 82 | elseif t == "json" then |
| 83 | return json.decode(value); |
| 84 | end |
| 85 | end |
| 86 | |
| 87 | local function getsql(sql, ...) |
| 88 | if params.driver == "PostgreSQL" then |
| 89 | sql = sql:gsub("`", "\""); |
| 90 | end |
| 91 | -- do prepared statement stuff |
| 92 | local stmt, err = connection:prepare(sql); |
| 93 | if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end |
| 94 | -- run query |
| 95 | local ok, err = stmt:execute(host or "", user or "", store or "", ...); |
| 96 | if not ok then return nil, err; end |
| 97 | |
| 98 | return stmt; |
| 99 | end |
| 100 | local function setsql(sql, ...) |
| 101 | local stmt, err = getsql(sql, ...); |
| 102 | if not stmt then return stmt, err; end |
| 103 | return stmt:affected(); |
| 104 | end |
| 105 | local function transact(...) |
| 106 | -- ... |
| 107 | end |
| 108 | local function rollback(...) |
| 109 | connection:rollback(); -- FIXME check for rollback error? |
| 110 | return ...; |
| 111 | end |
| 112 | local function commit(...) |
| 113 | if not connection:commit() then return nil, "SQL commit failed"; end |
| 114 | return ...; |
| 115 | end |
| 116 | |
| 117 | local keyval_store = {}; |
| 118 | keyval_store.__index = keyval_store; |
| 119 | function keyval_store:get(username) |
| 120 | user,store = username,self.store; |
| 121 | local stmt, err = getsql("SELECT * FROM `Prosody` WHERE `host`=? AND `user`=? AND `store`=?"); |
| 122 | if not stmt then return nil, err; end |
| 123 | |
| 124 | local haveany; |
| 125 | local result = {}; |
| 126 | for row in stmt:rows(true) do |
| 127 | haveany = true; |
| 128 | local k = row.key; |
| 129 | local v = deserialize(row.type, row.value); |
| 130 | if k and v then |
| 131 | if k ~= "" then result[k] = v; elseif type(v) == "table" then |
| 132 | for a,b in pairs(v) do |
| 133 | result[a] = b; |
| 134 | end |
| 135 | end |
| 136 | end |
| 137 | end |
| 138 | return commit(haveany and result or nil); |
| 139 | end |
| 140 | function keyval_store:set(username, data) |
| 141 | user,store = username,self.store; |
| 142 | -- start transaction |
| 143 | local affected, err = setsql("DELETE FROM `Prosody` WHERE `host`=? AND `user`=? AND `store`=?"); |
| 144 | |
| 145 | if data and next(data) ~= nil then |
| 146 | local extradata = {}; |
| 147 | for key, value in pairs(data) do |
| 148 | if type(key) == "string" and key ~= "" then |
| 149 | local t, value = serialize(value); |
| 150 | if not t then return rollback(t, value); end |
| 151 | local ok, err = setsql("INSERT INTO `Prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value); |
| 152 | if not ok then return rollback(ok, err); end |
| 153 | else |
| 154 | extradata[key] = value; |
| 155 | end |
| 156 | end |
| 157 | if next(extradata) ~= nil then |
| 158 | local t, extradata = serialize(extradata); |
| 159 | if not t then return rollback(t, extradata); end |
| 160 | local ok, err = setsql("INSERT INTO `Prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", "", t, extradata); |
| 161 | if not ok then return rollback(ok, err); end |
| 162 | end |
| 163 | end |
| 164 | return commit(true); |
| 165 | end |
| 166 | |
| 167 | local map_store = {}; |
| 168 | map_store.__index = map_store; |
| 169 | function map_store:get(username, key) |
| 170 | user,store = username,self.store; |
| 171 | local stmt, err = getsql("SELECT * FROM `Prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); |
| 172 | if not stmt then return nil, err; end |
| 173 | |
| 174 | local haveany; |
| 175 | local result = {}; |
| 176 | for row in stmt:rows(true) do |
| 177 | haveany = true; |
| 178 | local k = row.key; |
| 179 | local v = deserialize(row.type, row.value); |
| 180 | if k and v then |
| 181 | if k ~= "" then result[k] = v; elseif type(v) == "table" then |
| 182 | for a,b in pairs(v) do |
| 183 | result[a] = b; |
| 184 | end |
| 185 | end |
| 186 | end |
| 187 | end |
| 188 | return commit(haveany and result[key] or nil); |
| 189 | end |
| 190 | function map_store:set(username, key, data) |
| 191 | user,store = username,self.store; |
| 192 | -- start transaction |
| 193 | local affected, err = setsql("DELETE FROM `Prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); |
| 194 | |
| 195 | if data and next(data) ~= nil then |
| 196 | if type(key) == "string" and key ~= "" then |
| 197 | local t, value = serialize(data); |
| 198 | if not t then return rollback(t, value); end |
| 199 | local ok, err = setsql("INSERT INTO `Prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value); |
| 200 | if not ok then return rollback(ok, err); end |
| 201 | else |
| 202 | -- TODO non-string keys |
| 203 | end |
| 204 | end |
| 205 | return commit(true); |
| 206 | end |
| 207 | |
| 208 | local list_store = {}; |
| 209 | list_store.__index = list_store; |
| 210 | function list_store:scan(username, from, to, jid, typ) |
| 211 | user,store = username,self.store; |
| 212 | |
| 213 | local cols = {"from", "to", "jid", "typ"}; |
| 214 | local vals = { from , to , jid , typ }; |
| 215 | local stmt, err; |
| 216 | local query = "SELECT * FROM `ProsodyArchive` WHERE `host`=? AND `user`=? AND `store`=?"; |
| 217 | |
| 218 | query = query.." ORDER BY time"; |
| 219 | --local stmt, err = getsql("SELECT * FROM `Prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); |
| 220 | |
| 221 | return nil, "not-implemented" |
| 222 | end |
| 223 | |
| 224 | local driver = { name = "sql" }; |
| 225 | |
| 226 | function driver:open(store, typ) |
| 227 | if not typ then -- default key-value store |
| 228 | return setmetatable({ store = store }, keyval_store); |
| 229 | end |
| 230 | return nil, "unsupported-store"; |
| 231 | end |
| 232 | |
| 233 | module:add_item("data-driver", driver); |