PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/mod_storage_sql1.lua

http://github.com/bjc/prosody
Lua | 414 lines | 374 code | 19 blank | 21 comment | 37 complexity | d8a0d151ab4350a9d8d834ca8356fe22 MD5 | raw file
  1. --[[
  2. DB Tables:
  3. Prosody - key-value, map
  4. | host | user | store | key | type | value |
  5. ProsodyArchive - list
  6. | host | user | store | key | time | stanzatype | jsonvalue |
  7. Mapping:
  8. Roster - Prosody
  9. | host | user | "roster" | "contactjid" | type | value |
  10. | host | user | "roster" | NULL | "json" | roster[false] data |
  11. Account - Prosody
  12. | host | user | "accounts" | "username" | type | value |
  13. Offline - ProsodyArchive
  14. | host | user | "offline" | "contactjid" | time | "message" | json|XML |
  15. ]]
  16. local type = type;
  17. local tostring = tostring;
  18. local tonumber = tonumber;
  19. local pairs = pairs;
  20. local next = next;
  21. local setmetatable = setmetatable;
  22. local xpcall = xpcall;
  23. local json = require "util.json";
  24. local build_url = require"socket.url".build;
  25. local DBI;
  26. local connection;
  27. local host,user,store = module.host;
  28. local params = module:get_option("sql");
  29. local dburi;
  30. local connections = module:shared "/*/sql/connection-cache";
  31. local function db2uri(params)
  32. return build_url{
  33. scheme = params.driver,
  34. user = params.username,
  35. password = params.password,
  36. host = params.host,
  37. port = params.port,
  38. path = params.database,
  39. };
  40. end
  41. local resolve_relative_path = require "util.paths".resolve_relative_path;
  42. local function test_connection()
  43. if not connection then return nil; end
  44. if connection:ping() then
  45. return true;
  46. else
  47. module:log("debug", "Database connection closed");
  48. connection = nil;
  49. connections[dburi] = nil;
  50. end
  51. end
  52. local function connect()
  53. if not test_connection() then
  54. prosody.unlock_globals();
  55. local dbh, err = DBI.Connect(
  56. params.driver, params.database,
  57. params.username, params.password,
  58. params.host, params.port
  59. );
  60. prosody.lock_globals();
  61. if not dbh then
  62. module:log("debug", "Database connection failed: %s", tostring(err));
  63. return nil, err;
  64. end
  65. module:log("debug", "Successfully connected to database");
  66. dbh:autocommit(false); -- don't commit automatically
  67. connection = dbh;
  68. connections[dburi] = dbh;
  69. end
  70. return connection;
  71. end
  72. local function create_table()
  73. if not module:get_option("sql_manage_tables", true) then
  74. return;
  75. end
  76. local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);";
  77. if params.driver == "PostgreSQL" then
  78. create_sql = create_sql:gsub("`", "\"");
  79. elseif params.driver == "MySQL" then
  80. create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT");
  81. end
  82. local stmt, err = connection:prepare(create_sql);
  83. if stmt then
  84. local ok = stmt:execute();
  85. local commit_ok = connection:commit();
  86. if ok and commit_ok then
  87. module:log("info", "Initialized new %s database with prosody table", params.driver);
  88. local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)";
  89. if params.driver == "PostgreSQL" then
  90. index_sql = index_sql:gsub("`", "\"");
  91. elseif params.driver == "MySQL" then
  92. index_sql = index_sql:gsub("`([,)])", "`(20)%1");
  93. end
  94. local stmt, err = connection:prepare(index_sql);
  95. local ok, commit_ok, commit_err;
  96. if stmt then
  97. ok, err = stmt:execute();
  98. commit_ok, commit_err = connection:commit();
  99. end
  100. if not(ok and commit_ok) then
  101. module:log("warn", "Failed to create index (%s), lookups may not be optimised", err or commit_err);
  102. end
  103. elseif params.driver == "MySQL" then -- COMPAT: Upgrade tables from 0.8.0
  104. -- Failed to create, but check existing MySQL table here
  105. local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
  106. local ok = stmt:execute();
  107. local commit_ok = connection:commit();
  108. if ok and commit_ok then
  109. if stmt:rowcount() > 0 then
  110. module:log("info", "Upgrading database schema...");
  111. local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT");
  112. local ok, err = stmt:execute();
  113. local commit_ok = connection:commit();
  114. if ok and commit_ok then
  115. module:log("info", "Database table automatically upgraded");
  116. else
  117. module:log("error", "Failed to upgrade database schema (%s), please see "
  118. .."https://prosody.im/doc/mysql for help",
  119. err or "unknown error");
  120. end
  121. end
  122. repeat until not stmt:fetch();
  123. end
  124. end
  125. elseif params.driver ~= "SQLite3" then -- SQLite normally fails to prepare for existing table
  126. module:log("warn", "Prosody was not able to automatically check/create the database table (%s), "
  127. .."see https://prosody.im/doc/modules/mod_storage_sql#table_management for help.",
  128. err or "unknown error");
  129. end
  130. end
  131. do -- process options to get a db connection
  132. local ok;
  133. prosody.unlock_globals();
  134. ok, DBI = pcall(require, "DBI");
  135. if not ok then
  136. package.loaded["DBI"] = {};
  137. module:log("error", "Failed to load the LuaDBI library for accessing SQL databases: %s", DBI);
  138. module:log("error", "More information on installing LuaDBI can be found at https://prosody.im/doc/depends#luadbi");
  139. end
  140. prosody.lock_globals();
  141. if not ok or not DBI.Connect then
  142. return; -- Halt loading of this module
  143. end
  144. params = params or { driver = "SQLite3" };
  145. if params.driver == "SQLite3" then
  146. params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
  147. end
  148. assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
  149. dburi = db2uri(params);
  150. connection = connections[dburi];
  151. assert(connect());
  152. -- Automatically create table, ignore failure (table probably already exists)
  153. create_table();
  154. end
  155. local function serialize(value)
  156. local t = type(value);
  157. if t == "string" or t == "boolean" or t == "number" then
  158. return t, tostring(value);
  159. elseif t == "table" then
  160. local value,err = json.encode(value);
  161. if value then return "json", value; end
  162. return nil, err;
  163. end
  164. return nil, "Unhandled value type: "..t;
  165. end
  166. local function deserialize(t, value)
  167. if t == "string" then return value;
  168. elseif t == "boolean" then
  169. if value == "true" then return true;
  170. elseif value == "false" then return false; end
  171. elseif t == "number" then return tonumber(value);
  172. elseif t == "json" then
  173. return json.decode(value);
  174. end
  175. end
  176. local function dosql(sql, ...)
  177. if params.driver == "PostgreSQL" then
  178. sql = sql:gsub("`", "\"");
  179. end
  180. -- do prepared statement stuff
  181. local stmt, err = connection:prepare(sql);
  182. if not stmt and not test_connection() then error("connection failed"); end
  183. if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
  184. -- run query
  185. local ok, err = stmt:execute(...);
  186. if not ok and not test_connection() then error("connection failed"); end
  187. if not ok then return nil, err; end
  188. return stmt;
  189. end
  190. local function getsql(sql, ...)
  191. return dosql(sql, host or "", user or "", store or "", ...);
  192. end
  193. local function setsql(sql, ...)
  194. local stmt, err = getsql(sql, ...);
  195. if not stmt then return stmt, err; end
  196. return stmt:affected();
  197. end
  198. local function transact(...)
  199. -- ...
  200. end
  201. local function rollback(...)
  202. if connection then connection:rollback(); end -- FIXME check for rollback error?
  203. return ...;
  204. end
  205. local function commit(...)
  206. local success,err = connection:commit();
  207. if not success then return nil, "SQL commit failed: "..tostring(err); end
  208. return ...;
  209. end
  210. local function keyval_store_get()
  211. local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
  212. if not stmt then return rollback(nil, err); end
  213. local haveany;
  214. local result = {};
  215. for row in stmt:rows(true) do
  216. haveany = true;
  217. local k = row.key;
  218. local v = deserialize(row.type, row.value);
  219. if k and v then
  220. if k ~= "" then result[k] = v; elseif type(v) == "table" then
  221. for a,b in pairs(v) do
  222. result[a] = b;
  223. end
  224. end
  225. end
  226. end
  227. return commit(haveany and result or nil);
  228. end
  229. local function keyval_store_set(data)
  230. local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
  231. if not affected then return rollback(affected, err); end
  232. if data and next(data) ~= nil then
  233. local extradata = {};
  234. for key, value in pairs(data) do
  235. if type(key) == "string" and key ~= "" then
  236. local t, value = serialize(value);
  237. if not t then return rollback(t, value); end
  238. local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
  239. if not ok then return rollback(ok, err); end
  240. else
  241. extradata[key] = value;
  242. end
  243. end
  244. if next(extradata) ~= nil then
  245. local t, extradata = serialize(extradata);
  246. if not t then return rollback(t, extradata); end
  247. local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", "", t, extradata);
  248. if not ok then return rollback(ok, err); end
  249. end
  250. end
  251. return commit(true);
  252. end
  253. local keyval_store = {};
  254. keyval_store.__index = keyval_store;
  255. function keyval_store:get(username)
  256. user,store = username,self.store;
  257. if not connection and not connect() then return nil, "Unable to connect to database"; end
  258. local success, ret, err = xpcall(keyval_store_get, debug.traceback);
  259. if not connection and connect() then
  260. success, ret, err = xpcall(keyval_store_get, debug.traceback);
  261. end
  262. if success then return ret, err; else return rollback(nil, ret); end
  263. end
  264. function keyval_store:set(username, data)
  265. user,store = username,self.store;
  266. if not connection and not connect() then return nil, "Unable to connect to database"; end
  267. local success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
  268. if not connection and connect() then
  269. success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
  270. end
  271. if success then return ret, err; else return rollback(nil, ret); end
  272. end
  273. function keyval_store:users()
  274. local stmt, err = dosql("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store);
  275. if not stmt then
  276. return rollback(nil, err);
  277. end
  278. local next = stmt:rows();
  279. return commit(function()
  280. local row = next();
  281. return row and row[1];
  282. end);
  283. end
  284. local function map_store_get(key)
  285. local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
  286. if not stmt then return rollback(nil, err); end
  287. local haveany;
  288. local result = {};
  289. for row in stmt:rows(true) do
  290. haveany = true;
  291. local k = row.key;
  292. local v = deserialize(row.type, row.value);
  293. if k and v then
  294. if k ~= "" then result[k] = v; elseif type(v) == "table" then
  295. for a,b in pairs(v) do
  296. result[a] = b;
  297. end
  298. end
  299. end
  300. end
  301. return commit(haveany and result[key] or nil);
  302. end
  303. local function map_store_set(key, data)
  304. local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
  305. if not affected then return rollback(affected, err); end
  306. if data and next(data) ~= nil then
  307. if type(key) == "string" and key ~= "" then
  308. local t, value = serialize(data);
  309. if not t then return rollback(t, value); end
  310. local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
  311. if not ok then return rollback(ok, err); end
  312. else
  313. -- TODO non-string keys
  314. end
  315. end
  316. return commit(true);
  317. end
  318. local map_store = {};
  319. map_store.__index = map_store;
  320. function map_store:get(username, key)
  321. user,store = username,self.store;
  322. local success, ret, err = xpcall(function() return map_store_get(key); end, debug.traceback);
  323. if success then return ret, err; else return rollback(nil, ret); end
  324. end
  325. function map_store:set(username, key, data)
  326. user,store = username,self.store;
  327. local success, ret, err = xpcall(function() return map_store_set(key, data); end, debug.traceback);
  328. if success then return ret, err; else return rollback(nil, ret); end
  329. end
  330. local list_store = {};
  331. list_store.__index = list_store;
  332. function list_store:scan(username, from, to, jid, typ)
  333. user,store = username,self.store;
  334. local cols = {"from", "to", "jid", "typ"};
  335. local vals = { from , to , jid , typ };
  336. local stmt, err;
  337. local query = "SELECT * FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=?";
  338. query = query.." ORDER BY time";
  339. --local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
  340. return nil, "not-implemented"
  341. end
  342. local driver = {};
  343. function driver:open(store, typ)
  344. if typ and typ ~= "keyval" then
  345. return nil, "unsupported-store";
  346. end
  347. return setmetatable({ store = store }, keyval_store);
  348. end
  349. function driver:stores(username)
  350. local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" ..
  351. (username == true and "!=?" or "=?");
  352. if username == true or not username then
  353. username = "";
  354. end
  355. local stmt, err = dosql(sql, host, username);
  356. if not stmt then
  357. return rollback(nil, err);
  358. end
  359. local next = stmt:rows();
  360. return commit(function()
  361. local row = next();
  362. return row and row[1];
  363. end);
  364. end
  365. function driver:purge(username)
  366. local stmt, err = dosql("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username);
  367. if not stmt then return rollback(stmt, err); end
  368. local changed, err = stmt:affected();
  369. if not changed then return rollback(changed, err); end
  370. return commit(true, changed);
  371. end
  372. module:provides("storage", driver);