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");
10 local DBI = require "DBI";
11 -- This loads all available drivers while globals are unlocked
12 -- LuaDBI should be fixed to not set globals.
14 local build_url = require "socket.url".build;
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
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;
52 [Integer] = Integer();
56 function Column(definition)
57 return setmetatable(definition, column_mt);
59 function Table(definition)
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;
68 return setmetatable({ __table__ = definition, c = c, name = definition.name }, table_mt);
70 function Index(definition)
71 return setmetatable(definition, index_mt);
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);
79 return 'Table{ '..t_concat(s, ", ")..' }'
81 table_mt.__index = {};
82 function table_mt.__index:create(engine)
83 return engine:_create_table(self);
85 function table_mt:__call(...)
88 function column_mt:__tostring()
89 return 'Column{ name="'..self.name..'", type="'..self.type..'" }'
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
95 -- return 'Index{ name="'..self.name..'", type="'..self.type..'" }'
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
107 username, password = authpart:match("([^:]*):(.*)");
108 username = username or authpart;
109 password = password and urldecode(password);
112 host, port = hostpart:match("([^:]*):(.*)");
113 host = host or hostpart;
114 port = port and assert(tonumber(port), "Invalid URL format");
117 scheme = scheme:lower();
118 username = username; password = password;
119 host = host; port = port;
120 database = #database > 0 and database or nil;
124 --[[local session = {};
126 function session.query(...)
128 local query = setmetatable({ __rets = rets, __filters }, query_mt);
133 local function db2uri(params)
135 scheme = params.driver,
136 user = params.username,
137 password = params.password,
140 path = params.database,
145 function engine:connect()
146 if self.conn then return true; end
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
155 if not dbh then return nil, err; end
156 dbh:autocommit(false); -- don't commit automatically
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;
166 local stmt = prepared[sql];
169 stmt, err = self.conn:prepare(sql);
170 if not stmt then return stmt, err; end
171 prepared[sql] = stmt;
174 local success, err = stmt:execute(...);
175 if not success then return success, err; end
179 local result_mt = { __index = {
180 affected = function(self) return self.__stmt:affected(); end;
181 rowcount = function(self) return self.__stmt:rowcount(); end;
184 function engine:execute_query(sql, ...)
185 if self.params.driver == "PostgreSQL" then
186 sql = sql:gsub("`", "\"");
188 local stmt = assert(self.conn:prepare(sql));
189 assert(stmt:execute(...));
192 function engine:execute_update(sql, ...)
193 if self.params.driver == "PostgreSQL" then
194 sql = sql:gsub("`", "\"");
196 local prepared = self.prepared;
197 local stmt = prepared[sql];
199 stmt = assert(self.conn:prepare(sql));
200 prepared[sql] = stmt;
202 assert(stmt:execute(...));
203 return setmetatable({ __stmt = stmt }, result_mt);
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
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;
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;
226 log("debug", "SQL transaction failure [%s]: %s", tostring(func), a);
227 if self.conn then self.conn:rollback(); end
231 function engine:transaction(...)
232 local a,b = self:_transaction(...);
234 local conn = self.conn;
235 if not conn or not conn:ping() then
237 a,b = self:_transaction(...);
242 function engine:_create_index(index)
243 local sql = "CREATE INDEX `"..index.name.."` ON `"..index.table.."` (";
245 sql = sql.."`"..index[i].."`";
246 if i ~= #index then sql = sql..", "; end
249 if self.params.driver == "PostgreSQL" then
250 sql = sql:gsub("`", "\"");
251 elseif self.params.driver == "MySQL" then
252 sql = sql:gsub("`([,)])", "`(20)%1");
255 sql = sql:gsub("^CREATE", "CREATE UNIQUE");
258 return self:execute(sql);
260 function engine:_create_table(table)
261 local sql = "CREATE TABLE `"..table.name.."` (";
262 for i,col in ipairs(table.c) do
263 local col_type = col.type;
264 if col_type == "MEDIUMTEXT" and self.params.driver ~= "MySQL" then
265 col_type = "TEXT"; -- MEDIUMTEXT is MySQL-specific
267 if col.auto_increment == true and self.params.driver == "PostgreSQL" then
268 col_type = "BIGSERIAL";
270 sql = sql.."`"..col.name.."` "..col_type;
271 if col.nullable == false then sql = sql.." NOT NULL"; end
272 if col.primary_key == true then sql = sql.." PRIMARY KEY"; end
273 if col.auto_increment == true then
274 if self.params.driver == "MySQL" then
275 sql = sql.." AUTO_INCREMENT";
276 elseif self.params.driver == "SQLite3" then
277 sql = sql.." AUTOINCREMENT";
280 if i ~= #table.c then sql = sql..", "; end
283 if self.params.driver == "PostgreSQL" then
284 sql = sql:gsub("`", "\"");
285 elseif self.params.driver == "MySQL" then
286 sql = sql:gsub(";$", " CHARACTER SET 'utf8' COLLATE 'utf8_bin';");
288 local success,err = self:execute(sql);
289 if not success then return success,err; end
290 for i,v in ipairs(table.__table__) do
292 self:_create_index(v);
297 function engine:set_encoding() -- to UTF-8
298 local driver = self.params.driver;
299 if driver == "SQLite3" then
300 return self:transaction(function()
301 if self:select"PRAGMA encoding;"()[1] == "UTF-8" then
302 self.charset = "utf8";
306 local set_names_query = "SET NAMES '%s';"
307 local charset = "utf8";
308 if driver == "MySQL" then
309 set_names_query = set_names_query:gsub(";$", " COLLATE 'utf8_bin';");
310 local ok, charsets = self:transaction(function()
311 return self:select"SELECT `CHARACTER_SET_NAME` FROM `information_schema`.`CHARACTER_SETS` WHERE `CHARACTER_SET_NAME` LIKE 'utf8%' ORDER BY MAXLEN DESC LIMIT 1;";
313 local row = ok and charsets();
314 charset = row and row[1] or charset;
316 self.charset = charset;
317 return self:transaction(function() return self:execute(set_names_query:format(charset)); end);
319 local engine_mt = { __index = engine };
321 local function db2uri(params)
323 scheme = params.driver,
324 user = params.username,
325 password = params.password,
328 path = params.database,
331 local engine_cache = {}; -- TODO make weak valued
332 function create_engine(self, params)
333 local url = db2uri(params);
334 if not engine_cache[url] then
335 local engine = setmetatable({ url = url, params = params }, engine_mt);
336 engine_cache[url] = engine;
338 return engine_cache[url];
344 Column { name="user_id", type=String(), primary_key=true };
347 print(Users.c.user_id)]]
349 --local engine = create_engine('postgresql://scott:tiger@localhost:5432/mydatabase');
350 --[[local engine = create_engine{ driver = "SQLite3", database = "./alchemy.sqlite" };
353 for row in assert(engine:execute("select * from sqlite_master")):rows(true) do
356 for k,v in pairs(row) do
364 Column { name="host", type="TEXT", nullable=false };
365 Column { name="user", type="TEXT", nullable=false };
366 Column { name="store", type="TEXT", nullable=false };
367 Column { name="key", type="TEXT", nullable=false };
368 Column { name="type", type="TEXT", nullable=false };
369 Column { name="value", type="TEXT", nullable=false };
370 Index { name="prosody_index", "host", "user", "store", "key" };
373 assert(engine:transaction(function()
374 assert(Prosody:create(engine));
377 for row in assert(engine:execute("select user from prosody")):rows(true) do
378 print("username:", row['username'])