Software / code / prosody
Comparison
util/sql.lua @ 5494:9916f0a2d178
mod_storage_sql2 (temporary name), sql.lib, util.sql: New SQL API supporting cross-module connection sharing, transactions and Things - a work in progress
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Thu, 18 Apr 2013 11:13:40 +0100 |
| child | 5739:2e9ad9cb206f |
comparison
equal
deleted
inserted
replaced
| 5492:5d0cc5de5c51 | 5494:9916f0a2d178 |
|---|---|
| 1 | |
| 2 local setmetatable, getmetatable = setmetatable, getmetatable; | |
| 3 local ipairs, unpack, select = ipairs, unpack, select; | |
| 4 local tonumber, tostring = tonumber, tostring; | |
| 5 local assert, xpcall, debug_traceback = assert, xpcall, debug.traceback; | |
| 6 local t_concat = table.concat; | |
| 7 local s_char = string.char; | |
| 8 local log = require "util.logger".init("sql"); | |
| 9 | |
| 10 local DBI = require "DBI"; | |
| 11 -- This loads all available drivers while globals are unlocked | |
| 12 -- LuaDBI should be fixed to not set globals. | |
| 13 DBI.Drivers(); | |
| 14 local build_url = require "socket.url".build; | |
| 15 | |
| 16 module("sql") | |
| 17 | |
| 18 local column_mt = {}; | |
| 19 local table_mt = {}; | |
| 20 local query_mt = {}; | |
| 21 --local op_mt = {}; | |
| 22 local index_mt = {}; | |
| 23 | |
| 24 function is_column(x) return getmetatable(x)==column_mt; end | |
| 25 function is_index(x) return getmetatable(x)==index_mt; end | |
| 26 function is_table(x) return getmetatable(x)==table_mt; end | |
| 27 function is_query(x) return getmetatable(x)==query_mt; end | |
| 28 --function is_op(x) return getmetatable(x)==op_mt; end | |
| 29 --function expr(...) return setmetatable({...}, op_mt); end | |
| 30 function Integer(n) return "Integer()" end | |
| 31 function String(n) return "String()" end | |
| 32 | |
| 33 --[[local ops = { | |
| 34 __add = function(a, b) return "("..a.."+"..b..")" end; | |
| 35 __sub = function(a, b) return "("..a.."-"..b..")" end; | |
| 36 __mul = function(a, b) return "("..a.."*"..b..")" end; | |
| 37 __div = function(a, b) return "("..a.."/"..b..")" end; | |
| 38 __mod = function(a, b) return "("..a.."%"..b..")" end; | |
| 39 __pow = function(a, b) return "POW("..a..","..b..")" end; | |
| 40 __unm = function(a) return "NOT("..a..")" end; | |
| 41 __len = function(a) return "COUNT("..a..")" end; | |
| 42 __eq = function(a, b) return "("..a.."=="..b..")" end; | |
| 43 __lt = function(a, b) return "("..a.."<"..b..")" end; | |
| 44 __le = function(a, b) return "("..a.."<="..b..")" end; | |
| 45 }; | |
| 46 | |
| 47 local functions = { | |
| 48 | |
| 49 }; | |
| 50 | |
| 51 local cmap = { | |
| 52 [Integer] = Integer(); | |
| 53 [String] = String(); | |
| 54 };]] | |
| 55 | |
| 56 function Column(definition) | |
| 57 return setmetatable(definition, column_mt); | |
| 58 end | |
| 59 function Table(definition) | |
| 60 local c = {} | |
| 61 for i,col in ipairs(definition) do | |
| 62 if is_column(col) then | |
| 63 c[i], c[col.name] = col, col; | |
| 64 elseif is_index(col) then | |
| 65 col.table = definition.name; | |
| 66 end | |
| 67 end | |
| 68 return setmetatable({ __table__ = definition, c = c, name = definition.name }, table_mt); | |
| 69 end | |
| 70 function Index(definition) | |
| 71 return setmetatable(definition, index_mt); | |
| 72 end | |
| 73 | |
| 74 function table_mt:__tostring() | |
| 75 local s = { 'name="'..self.__table__.name..'"' } | |
| 76 for i,col in ipairs(self.__table__) do | |
| 77 s[#s+1] = tostring(col); | |
| 78 end | |
| 79 return 'Table{ '..t_concat(s, ", ")..' }' | |
| 80 end | |
| 81 table_mt.__index = {}; | |
| 82 function table_mt.__index:create(engine) | |
| 83 return engine:_create_table(self); | |
| 84 end | |
| 85 function table_mt:__call(...) | |
| 86 -- TODO | |
| 87 end | |
| 88 function column_mt:__tostring() | |
| 89 return 'Column{ name="'..self.name..'", type="'..self.type..'" }' | |
| 90 end | |
| 91 function index_mt:__tostring() | |
| 92 local s = 'Index{ name="'..self.name..'"'; | |
| 93 for i=1,#self do s = s..', "'..self[i]:gsub("[\\\"]", "\\%1")..'"'; end | |
| 94 return s..' }'; | |
| 95 -- return 'Index{ name="'..self.name..'", type="'..self.type..'" }' | |
| 96 end | |
| 97 -- | |
| 98 | |
| 99 local function urldecode(s) return s and (s:gsub("%%(%x%x)", function (c) return s_char(tonumber(c,16)); end)); end | |
| 100 local function parse_url(url) | |
| 101 local scheme, secondpart, database = url:match("^([%w%+]+)://([^/]*)/?(.*)"); | |
| 102 assert(scheme, "Invalid URL format"); | |
| 103 local username, password, host, port; | |
| 104 local authpart, hostpart = secondpart:match("([^@]+)@([^@+])"); | |
| 105 if not authpart then hostpart = secondpart; end | |
| 106 if authpart then | |
| 107 username, password = authpart:match("([^:]*):(.*)"); | |
| 108 username = username or authpart; | |
| 109 password = password and urldecode(password); | |
| 110 end | |
| 111 if hostpart then | |
| 112 host, port = hostpart:match("([^:]*):(.*)"); | |
| 113 host = host or hostpart; | |
| 114 port = port and assert(tonumber(port), "Invalid URL format"); | |
| 115 end | |
| 116 return { | |
| 117 scheme = scheme:lower(); | |
| 118 username = username; password = password; | |
| 119 host = host; port = port; | |
| 120 database = #database > 0 and database or nil; | |
| 121 }; | |
| 122 end | |
| 123 | |
| 124 --[[local session = {}; | |
| 125 | |
| 126 function session.query(...) | |
| 127 local rets = {...}; | |
| 128 local query = setmetatable({ __rets = rets, __filters }, query_mt); | |
| 129 return query; | |
| 130 end | |
| 131 -- | |
| 132 | |
| 133 local function db2uri(params) | |
| 134 return build_url{ | |
| 135 scheme = params.driver, | |
| 136 user = params.username, | |
| 137 password = params.password, | |
| 138 host = params.host, | |
| 139 port = params.port, | |
| 140 path = params.database, | |
| 141 }; | |
| 142 end]] | |
| 143 | |
| 144 local engine = {}; | |
| 145 function engine:connect() | |
| 146 if self.conn then return true; end | |
| 147 | |
| 148 local params = self.params; | |
| 149 assert(params.driver, "no driver") | |
| 150 local dbh, err = DBI.Connect( | |
| 151 params.driver, params.database, | |
| 152 params.username, params.password, | |
| 153 params.host, params.port | |
| 154 ); | |
| 155 if not dbh then return nil, err; end | |
| 156 dbh:autocommit(false); -- don't commit automatically | |
| 157 self.conn = dbh; | |
| 158 self.prepared = {}; | |
| 159 return true; | |
| 160 end | |
| 161 function engine:execute(sql, ...) | |
| 162 local success, err = self:connect(); | |
| 163 if not success then return success, err; end | |
| 164 local prepared = self.prepared; | |
| 165 | |
| 166 local stmt = prepared[sql]; | |
| 167 if not stmt then | |
| 168 local err; | |
| 169 stmt, err = self.conn:prepare(sql); | |
| 170 if not stmt then return stmt, err; end | |
| 171 prepared[sql] = stmt; | |
| 172 end | |
| 173 | |
| 174 local success, err = stmt:execute(...); | |
| 175 if not success then return success, err; end | |
| 176 return stmt; | |
| 177 end | |
| 178 | |
| 179 local result_mt = { __index = { | |
| 180 affected = function(self) return self.__affected; end; | |
| 181 rowcount = function(self) return self.__rowcount; end; | |
| 182 } }; | |
| 183 | |
| 184 function engine:execute_query(sql, ...) | |
| 185 if self.params.driver == "PostgreSQL" then | |
| 186 sql = sql:gsub("`", "\""); | |
| 187 end | |
| 188 local stmt = assert(self.conn:prepare(sql)); | |
| 189 assert(stmt:execute(...)); | |
| 190 return stmt:rows(); | |
| 191 end | |
| 192 function engine:execute_update(sql, ...) | |
| 193 if self.params.driver == "PostgreSQL" then | |
| 194 sql = sql:gsub("`", "\""); | |
| 195 end | |
| 196 local prepared = self.prepared; | |
| 197 local stmt = prepared[sql]; | |
| 198 if not stmt then | |
| 199 stmt = assert(self.conn:prepare(sql)); | |
| 200 prepared[sql] = stmt; | |
| 201 end | |
| 202 assert(stmt:execute(...)); | |
| 203 return setmetatable({ __affected = stmt:affected(), __rowcount = stmt:rowcount() }, result_mt); | |
| 204 end | |
| 205 engine.insert = engine.execute_update; | |
| 206 engine.select = engine.execute_query; | |
| 207 engine.delete = engine.execute_update; | |
| 208 engine.update = engine.execute_update; | |
| 209 function engine:_transaction(func, ...) | |
| 210 if not self.conn then | |
| 211 local a,b = self:connect(); | |
| 212 if not a then return a,b; end | |
| 213 end | |
| 214 --assert(not self.__transaction, "Recursive transactions not allowed"); | |
| 215 local args, n_args = {...}, select("#", ...); | |
| 216 local function f() return func(unpack(args, 1, n_args)); end | |
| 217 self.__transaction = true; | |
| 218 local success, a, b, c = xpcall(f, debug_traceback); | |
| 219 self.__transaction = nil; | |
| 220 if success then | |
| 221 log("debug", "SQL transaction success [%s]", tostring(func)); | |
| 222 local ok, err = self.conn:commit(); | |
| 223 if not ok then return ok, err; end -- commit failed | |
| 224 return success, a, b, c; | |
| 225 else | |
| 226 log("debug", "SQL transaction failure [%s]: %s", tostring(func), a); | |
| 227 if self.conn then self.conn:rollback(); end | |
| 228 return success, a; | |
| 229 end | |
| 230 end | |
| 231 function engine:transaction(...) | |
| 232 local a,b = self:_transaction(...); | |
| 233 if not a then | |
| 234 local conn = self.conn; | |
| 235 if not conn or not conn:ping() then | |
| 236 self.conn = nil; | |
| 237 a,b = self:_transaction(...); | |
| 238 end | |
| 239 end | |
| 240 return a,b; | |
| 241 end | |
| 242 function engine:_create_index(index) | |
| 243 local sql = "CREATE INDEX `"..index.name.."` ON `"..index.table.."` ("; | |
| 244 for i=1,#index do | |
| 245 sql = sql.."`"..index[i].."`"; | |
| 246 if i ~= #index then sql = sql..", "; end | |
| 247 end | |
| 248 sql = sql..");" | |
| 249 if self.params.driver == "PostgreSQL" then | |
| 250 sql = sql:gsub("`", "\""); | |
| 251 elseif self.params.driver == "MySQL" then | |
| 252 sql = sql:gsub("`([,)])", "`(20)%1"); | |
| 253 end | |
| 254 --print(sql); | |
| 255 return self:execute(sql); | |
| 256 end | |
| 257 function engine:_create_table(table) | |
| 258 local sql = "CREATE TABLE `"..table.name.."` ("; | |
| 259 for i,col in ipairs(table.c) do | |
| 260 sql = sql.."`"..col.name.."` "..col.type; | |
| 261 if col.nullable == false then sql = sql.." NOT NULL"; end | |
| 262 if i ~= #table.c then sql = sql..", "; end | |
| 263 end | |
| 264 sql = sql.. ");" | |
| 265 if self.params.driver == "PostgreSQL" then | |
| 266 sql = sql:gsub("`", "\""); | |
| 267 end | |
| 268 local success,err = self:execute(sql); | |
| 269 if not success then return success,err; end | |
| 270 for i,v in ipairs(table.__table__) do | |
| 271 if is_index(v) then | |
| 272 self:_create_index(v); | |
| 273 end | |
| 274 end | |
| 275 return success; | |
| 276 end | |
| 277 local engine_mt = { __index = engine }; | |
| 278 | |
| 279 local function db2uri(params) | |
| 280 return build_url{ | |
| 281 scheme = params.driver, | |
| 282 user = params.username, | |
| 283 password = params.password, | |
| 284 host = params.host, | |
| 285 port = params.port, | |
| 286 path = params.database, | |
| 287 }; | |
| 288 end | |
| 289 local engine_cache = {}; -- TODO make weak valued | |
| 290 function create_engine(self, params) | |
| 291 local url = db2uri(params); | |
| 292 if not engine_cache[url] then | |
| 293 local engine = setmetatable({ url = url, params = params }, engine_mt); | |
| 294 engine_cache[url] = engine; | |
| 295 end | |
| 296 return engine_cache[url]; | |
| 297 end | |
| 298 | |
| 299 | |
| 300 --[[Users = Table { | |
| 301 name="users"; | |
| 302 Column { name="user_id", type=String(), primary_key=true }; | |
| 303 }; | |
| 304 print(Users) | |
| 305 print(Users.c.user_id)]] | |
| 306 | |
| 307 --local engine = create_engine('postgresql://scott:tiger@localhost:5432/mydatabase'); | |
| 308 --[[local engine = create_engine{ driver = "SQLite3", database = "./alchemy.sqlite" }; | |
| 309 | |
| 310 local i = 0; | |
| 311 for row in assert(engine:execute("select * from sqlite_master")):rows(true) do | |
| 312 i = i+1; | |
| 313 print(i); | |
| 314 for k,v in pairs(row) do | |
| 315 print("",k,v); | |
| 316 end | |
| 317 end | |
| 318 print("---") | |
| 319 | |
| 320 Prosody = Table { | |
| 321 name="prosody"; | |
| 322 Column { name="host", type="TEXT", nullable=false }; | |
| 323 Column { name="user", type="TEXT", nullable=false }; | |
| 324 Column { name="store", type="TEXT", nullable=false }; | |
| 325 Column { name="key", type="TEXT", nullable=false }; | |
| 326 Column { name="type", type="TEXT", nullable=false }; | |
| 327 Column { name="value", type="TEXT", nullable=false }; | |
| 328 Index { name="prosody_index", "host", "user", "store", "key" }; | |
| 329 }; | |
| 330 --print(Prosody); | |
| 331 assert(engine:transaction(function() | |
| 332 assert(Prosody:create(engine)); | |
| 333 end)); | |
| 334 | |
| 335 for row in assert(engine:execute("select user from prosody")):rows(true) do | |
| 336 print("username:", row['username']) | |
| 337 end | |
| 338 --result.close();]] | |
| 339 | |
| 340 return _M; |