├── README.md ├── cache.lua ├── drivers └── mysql.lua ├── func.lua ├── init.lua ├── model.lua └── query.lua /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-orm 2 | ---- 3 | Simple ORM for [openresty](http://openresty.org) 4 | 5 | #Status 6 | ---- 7 | This library is not production ready. 8 | 9 | #Usage 10 | ---- 11 | 12 | ##connect to database: 13 | ``` 14 | local orm = require'orm'.open{ 15 | driver = 'mysql', 16 | port = 3306, 17 | host = '127.0.0.1', 18 | user = 'root', 19 | password = '123456', 20 | database = 'test', 21 | charset = 'utf8mb4', 22 | expires = 100 -- cache expires time period 23 | } 24 | ``` 25 | ##orm.expr(expression) 26 | 27 | Create sql expression which will never be escaped. 28 | 29 | ##orm.create_query(): 30 | 31 | This is the query builder, can now build select, update, insert, delete sql. 32 | ``` 33 | local sql = orm.create_query():from('table_name'):where('[id] = ?d', 99):one() 34 | -- SELSECT * FROM table_name WHERE `id` = 99 LIMIT 1 35 | ``` 36 | #####*from(table, alias):* 37 | ``` 38 | query:from('table') -- SELECT * FROM table 39 | query:from('[table]') -- SELECT * FROM `table` 40 | query:from(another_query:from('user', 'u')) -- SELECT * FROM (SELECT * FROM user) AS u 41 | ``` 42 | #####*select(fields):* 43 | ``` 44 | query:select('t1, t2, [t3]') -- SELECT t1, t2, `t3` ... 45 | ``` 46 | 47 | #####*where(cond, ...), and\_where(cond, ...), or_where(cond, ...):* 48 | ``` 49 | query:where('id = ?d or [key] like ?s', '10', '"lua-%-orm"') -- WHERE id = 10 or `key` like '\"lua-%-orm\"' 50 | query:where('id in (?t)', 1) -- WHERE id in (1) 51 | query:where('id in (?t)', {1, 2, 'a'}) --WHERE id in (1,2,'a') 52 | -- ?t can be ? if don't know type of param 53 | 54 | ``` 55 | - `?t` table {1,2,'a'} => 1,2,'a' 56 | - `?b` bool(0, 1), only false or nil will be converted to 0 57 | - `?e` expression: MAX(id) | MIN(id) ... 58 | - `?d` digit number, convert by tonumber 59 | - `?n` NULL, false and nil wil be converted to 'NULL', orther 'NOT NULL' 60 | - `?s` string, escape by ngx.quote_sql_str 61 | - `?` any, convert by guessing the value type 62 | 63 | THESE modifiers can be used in where/having/join methods 64 | 65 | #####*having(cond, ...), and_having(cond, ...), or_having(cond, ...):* 66 | 67 | just like `where` 68 | 69 | #####*join(tbl, cond, ...), left\_join, right\_join, inner_join:* 70 | 71 | JOIN `tbl` ON `cond` , `...` params will be used in `cond` 72 | 73 | #####*group_by(...), order_by(...):* 74 | 75 | Accept multiple `group by` | `order_by` expressions 76 | 77 | #####*limit([offset_num], limit_num):* 78 | 79 | `offset_num` is optional ( offset will have its own method in next commit ) 80 | 81 | #####*as(alias):* 82 | 83 | Set alias for `select` type sql. 84 | 85 | #####*set(key, value), set(hashmap):* 86 | 87 | Used in the `UPDATE tbl SET ...` sql. 88 | 89 | #####*values(hashmap):* 90 | 91 | Used in the `INSERT INTO tbl (...) VALUES (...)` 92 | 93 | #####*delete(tbl), update(tbl), insert(tbl):* 94 | 95 | Set the query type, `tbl` param is optional, which can also be setted by `from` method. 96 | 97 | #####*for_update():* 98 | 99 | `SELECT * FROM tbl WHERE id=1 FOR UPDATE` 100 | 101 | #####*build():* 102 | 103 | Return sql string 104 | 105 | #####*exec(callback), one(callback), all(callback):* 106 | 107 | Send query to database, `one`|`all` are only for `select` query. 108 | 109 | `callback` is the handler function called after db return results. It should accept two params (status, result) 110 | 111 | 112 | ##orm.define_model(table_name): 113 | 114 | `define_model` accept table name as paramater and cache table fields in lrucache. 115 | 116 | This method define a model like this: 117 | 118 | ``` 119 | local User = orm.define_model('tbl_user') 120 | -- fetch 121 | local ok, users = User.find_all('id > ?d', 10) 122 | if ok then 123 | for _, u in ipairs(users) do 124 | print(user.name) 125 | end 126 | end 127 | 128 | local ok, user = User.find_one('id = 10') 129 | if ok then 130 | user.name = 'new name' 131 | local ok, res = user:save() -- update user 132 | end 133 | 134 | -- update 135 | local attrs = { name = 'name updated' } 136 | User.update_where(attrs, 'id > ?', 10) 137 | -- delete 138 | User.delete_where('id = ?', 10) --delete all by condition 139 | user:delete() -- delete user instance 140 | 141 | -- create new 142 | local attrs = { name = 'new one' } 143 | local user = User.new(attrs) 144 | user:save() 145 | 146 | local user = User.new() 147 | user:load(attrs) -- same as User.new(attrs) 148 | 149 | ``` 150 | 151 | #TODO 152 | ---- 153 | 154 | * [db] postgresql support 155 | * [query] offset method 156 | * [model] event (after\_find, before\_save & etc) 157 | * [model] attributes validation 158 | 159 | -------------------------------------------------------------------------------- /cache.lua: -------------------------------------------------------------------------------- 1 | local lrucache = require "resty.lrucache" 2 | local error = error 3 | 4 | local c, err = lrucache.new(200) -- allow up to 200 items in the cache 5 | if not c then 6 | return error("failed to create the cache: " .. (err or "unknown")) 7 | end 8 | 9 | return c 10 | -------------------------------------------------------------------------------- /drivers/mysql.lua: -------------------------------------------------------------------------------- 1 | local mysql = require'resty.mysql' 2 | local quote_sql_str = ngx.quote_sql_str 3 | local assert = assert 4 | local ipairs = ipairs 5 | local table_concat = table.concat 6 | local table_insert = table.insert 7 | local lpeg = require'lpeg' 8 | local ngx = ngx 9 | 10 | local open = function(conf) 11 | local connect = function() 12 | local db, err = mysql:new() 13 | assert(not err, "failed to create: ", err) 14 | 15 | local ok, err, errno, sqlstate = db:connect(conf) 16 | assert(ok, "failed to connect: ", err, ": ", errno, " ", sqlstate) 17 | 18 | if conf.charset then 19 | if db:get_reused_times() == 0 then 20 | db:query("SET NAMES " .. conf.charset) 21 | end 22 | end 23 | 24 | return db 25 | end 26 | 27 | local config = function() 28 | return conf 29 | end 30 | 31 | local query = function(query_str) 32 | if conf.debug then 33 | ngx.log(ngx.DEBUG, '[SQL] ' .. query_str) 34 | end 35 | 36 | local db = connect() 37 | local res, err, errno, sqlstate = db:query(query_str) 38 | if not res then 39 | return false, table_concat({"bad result: " .. err, errno, sqlstate}, ', ') 40 | end 41 | 42 | if err == 'again' then res = { res } end 43 | while err == "again" do 44 | local tmp 45 | tmp, err, errno, sqlstate = db:read_result() 46 | if not tmp then 47 | return false, table_concat({"bad result: " .. err, errno, sqlstate}, ', ') 48 | end 49 | 50 | table_insert(res, tmp) 51 | end 52 | 53 | local ok, err = db:set_keepalive(10000, 50) 54 | if not ok then 55 | ngx.log(ngx.ERR, "failed to set keepalive: ", err) 56 | end 57 | 58 | return true, res 59 | end 60 | 61 | local escape_identity = function(id) 62 | local qchar = '`' 63 | local openp, endp = lpeg.P'[', lpeg.P']' 64 | local quote_pat = openp * lpeg.C(( 1 - endp)^1) * endp 65 | local repl = qchar .. '%1' .. qchar 66 | return lpeg.Cs((quote_pat/repl + 1)^0):match(id) 67 | end 68 | 69 | local get_schema = function(table_name) 70 | -- {Null="YES",Field="user_position",Type="varchar(45)",Extra="",Key="",Default=""} 71 | local ok, res = query('desc ' .. escape_identity(table_name)) 72 | assert(ok, res) 73 | 74 | local fields = { } 75 | for _, f in ipairs(res) do 76 | fields[f.Field] = f 77 | if f.Key == 'PRI' then 78 | if fields.__pk__ then 79 | error('not implement for tables have multiple pk') 80 | end 81 | fields.__pk__ = f.Field 82 | end 83 | end 84 | 85 | return fields 86 | end 87 | 88 | return { 89 | query = query; 90 | get_schema = get_schema; 91 | config = config; 92 | escape_identity = escape_identity; 93 | } 94 | end 95 | 96 | 97 | return open 98 | -------------------------------------------------------------------------------- /func.lua: -------------------------------------------------------------------------------- 1 | local table_insert = table.insert 2 | local unpack = unpack 3 | local ipairs = ipairs 4 | local pairs = pairs 5 | 6 | local _M = { } 7 | 8 | 9 | local function kmap(func, tbl) 10 | if not tbl then return end 11 | 12 | local res = {} 13 | for k, v in pairs(tbl) do 14 | local rk, rv = func(k, v) 15 | if rk then 16 | res[rk] = rv 17 | else 18 | table_insert(res, rv) 19 | end 20 | end 21 | 22 | return res 23 | end 24 | 25 | _M.kmap = kmap 26 | 27 | local function map(func, tbl) 28 | if not tbl then return end 29 | 30 | local res = {} 31 | for k, v in pairs(tbl) do 32 | local rv = func(v, k) 33 | if rv ~= nil then 34 | table_insert(res, rv) 35 | end 36 | end 37 | 38 | return res 39 | end 40 | 41 | _M.map = map 42 | 43 | local function filter(func, tbl) 44 | return kmap(function(k, v) 45 | local rk, rv = func(k, v) 46 | if rk then 47 | return rk, rv 48 | end 49 | end, tbl) 50 | end 51 | 52 | _M.filter = filter 53 | 54 | 55 | local function reduce(func, acc, tbl) 56 | if not tbl then return end 57 | 58 | for k, v in pairs(tbl) do 59 | acc = func(k, v, acc) 60 | end 61 | 62 | return acc 63 | end 64 | 65 | _M.reduce = reduce 66 | 67 | local function curry(func, ...) 68 | if select('#', ...) == 0 then return func end 69 | local args = { ... } 70 | return function( ... ) 71 | local clone = { unpack(args) } 72 | for _, v in ipairs{...} do table_insert(clone , v) end 73 | return func(unpack(clone)) 74 | end 75 | end 76 | _M.curry = curry 77 | 78 | local function chain(...) 79 | local args = { ... } 80 | return function(...) 81 | local real_arg = {...} 82 | for i=1, #args do 83 | real_arg = { args[i](unpack(real_arg)) } 84 | end 85 | return unpack(real_arg) 86 | end 87 | end 88 | _M.chain = chain 89 | 90 | 91 | _M.table_keys = curry(kmap, function(k, v) return nil, k end) 92 | _M.table_vals = curry(map, function(v) return v end) 93 | _M.table_clone = curry(kmap, function(k,v) return k, v end) 94 | 95 | 96 | return _M 97 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | local Query = require'orm.query' 2 | local Model = require'orm.model' 3 | local assert = assert 4 | local pcall = pcall 5 | 6 | local function open(conf) 7 | local driver = conf.driver 8 | assert(driver, "please specific db driver") 9 | 10 | local ok, db = pcall(require, 'orm.drivers.' .. driver) 11 | assert(ok, 'no driver for ' .. driver) 12 | 13 | local conn = db(conf) 14 | 15 | local create_query = function() 16 | return Query.create(conn) 17 | end 18 | 19 | local define_model = function(attrs) 20 | return Model(conn, create_query, attrs) 21 | end 22 | 23 | return { 24 | db = conn; 25 | create_query = create_query; 26 | define_model = define_model; 27 | expr = Query.expr; 28 | } 29 | end 30 | 31 | return { 32 | open = open; 33 | } 34 | -------------------------------------------------------------------------------- /model.lua: -------------------------------------------------------------------------------- 1 | local fun = require'orm.func' 2 | local cache = require'orm.cache' 3 | local table_concat = table.concat 4 | local setmetatable = setmetatable 5 | local rawget = rawget 6 | local assert = assert 7 | local type = type 8 | 9 | local function define_model(DB, Query, table_name) 10 | 11 | assert(type(table_name) == 'string', 'table name required') 12 | table_name = DB.escape_identity(table_name) 13 | 14 | local _init_model = function(Model) 15 | 16 | local attrs 17 | 18 | local conf = DB.config() 19 | local cache_key = table_concat({'orm', conf.host, conf.port, conf.database, table_name}, '^') 20 | local data, stale = cache:get(cache_key) 21 | 22 | if not data then 23 | attrs = DB.get_schema(table_name) 24 | cache:set(cache_key, fun.table_clone(attrs), conf.expires) 25 | else 26 | attrs = fun.table_clone(data) 27 | end 28 | 29 | assert(attrs, 'initializing model failed') 30 | assert(attrs.__pk__, 'primary key required') 31 | local pk = attrs.__pk__ 32 | attrs.__pk__ = nil 33 | 34 | local function filter_attrs(params) 35 | return fun.kmap(function(k, v) 36 | if type(k) == 'number' then 37 | return k, v 38 | elseif attrs[k] ~= nil then 39 | return k, v 40 | end 41 | end, params) 42 | end 43 | 44 | local function pop_models(ok, rows) 45 | if not ok then return ok, rows end 46 | 47 | return ok, fun.map(function(row) 48 | local model = Model.new(row, true) 49 | model:trigger('AfterFind') 50 | return model 51 | end, rows) 52 | end 53 | 54 | local function query() 55 | return Query():from(table_name) 56 | end 57 | 58 | Model.find = function() 59 | local q = query() 60 | getmetatable(q).__call = function(self) 61 | if self._state == 'select' then 62 | return pop_models(self:exec()) 63 | end 64 | return self:exec() 65 | end 66 | 67 | return q 68 | end 69 | 70 | Model.group = function(expr, cond, ...) 71 | local q = query():select(expr .. ' AS group__res') 72 | if cond then q:where(cond, ...) end 73 | 74 | local ok, res = q() 75 | if ok and #res > 0 then 76 | return res[1].group__res 77 | end 78 | return nil 79 | end 80 | 81 | Model.count = function(cond, ...) 82 | return Model.group('COUNT(*)', cond, ...) 83 | end 84 | 85 | Model.find_all = function(cond, ...) 86 | return Model.find():where(cond, ...)() 87 | end 88 | 89 | Model.find_one = function(cond, ...) 90 | local ok, records = Model.find():where(cond, ...):limit(1)() 91 | 92 | if ok then records = records[1] end 93 | return ok, records 94 | end 95 | 96 | Model.update_where = function(set, cond, ...) 97 | return query():update():where(cond, ...):set(set)() 98 | end 99 | 100 | Model.delete_where = function(cond, ...) 101 | return query():delete():where(cond, ...)() 102 | end 103 | 104 | Model.__index = function(self, key) 105 | if Model[key] then 106 | return Model[key] 107 | else 108 | return self.__attrs__[key] 109 | end 110 | end 111 | 112 | Model.__newindex = function(self, k, v) 113 | if attrs[k] ~= nil then 114 | self.__attrs__[k] = v 115 | self.__dirty_attrs__[k] = true 116 | end 117 | end 118 | 119 | function Model:set_dirty(attr) 120 | self.__dirty_attrs__[attr] = true 121 | end 122 | 123 | function Model:get_dirty_attrs() 124 | local count = 0 125 | local res = fun.kmap(function(k, v) 126 | count = count + 1 127 | return k, self.__attrs__[k] 128 | end, self.__dirty_attrs__) 129 | return res, count 130 | end 131 | 132 | function Model:save() 133 | if self[pk] then -- update 134 | 135 | self:trigger('BeforeSave') 136 | 137 | local res = "no dirty attributes" 138 | local ok = false 139 | local dirty_attrs, count = self:get_dirty_attrs() 140 | if count > 0 then 141 | ok, res = query():update():where(pk .. ' = ?d ', self[pk]):set(dirty_attrs)() 142 | 143 | if ok then 144 | self:set_none_dirty() 145 | end 146 | end 147 | 148 | return ok, res 149 | else -- insert 150 | 151 | self:trigger('BeforeSave') 152 | 153 | local ok, res = query():insert():values(self.__attrs__)() 154 | 155 | if ok then 156 | self[pk] = res.insert_id 157 | self:set_none_dirty() 158 | self.__is_new__ = false 159 | return ok, res 160 | else 161 | return false, res 162 | end 163 | end 164 | end 165 | 166 | function Model:set_none_dirty() 167 | self.__dirty_attrs__ = {} 168 | end 169 | 170 | function Model:delete() 171 | assert(self[pk], 'primary key ['.. pk .. '] required') 172 | 173 | return query():delete():where(pk .. '= ?d', self[pk])() 174 | end 175 | 176 | function Model:load(data) 177 | if type(data) == 'table' then 178 | fun.kmap(function(k, v) 179 | self[k] = v 180 | end, data) 181 | end 182 | end 183 | 184 | function Model:trigger(event, ...) 185 | local method = Model['on'..event] 186 | if type(method) == 'function' then 187 | return method(self, ...) 188 | end 189 | end 190 | 191 | function Model:is_new() 192 | return self.__is_new__ 193 | end 194 | 195 | Model.new = function(data, not_dirty) 196 | local instance = { __attrs__ = {}, __dirty_attrs__ = {} , __is_new__ = true } 197 | setmetatable(instance, Model) 198 | 199 | instance:load(data) 200 | if not_dirty then 201 | instance:set_none_dirty() 202 | -- while loading from db, records are not new 203 | instance.__is_new__ = false 204 | end 205 | 206 | return instance 207 | end 208 | 209 | setmetatable(Model, nil) 210 | end 211 | 212 | return setmetatable({}, { 213 | __index = function(self, key) 214 | _init_model(self) 215 | return rawget(self, key) 216 | end 217 | }) 218 | end 219 | 220 | return define_model 221 | 222 | -------------------------------------------------------------------------------- /query.lua: -------------------------------------------------------------------------------- 1 | local fun = require'orm.func' 2 | local lpeg = require'lpeg' 3 | 4 | local table_insert = table.insert 5 | local table_concat = table.concat 6 | local ipairs, pairs = ipairs, pairs 7 | local strlen = string.len 8 | local tostring = tostring 9 | local type, unpack = type, unpack 10 | local setmetatable = setmetatable 11 | 12 | local P, R, S = lpeg.P, lpeg.R, lpeg.S 13 | local C, Ct, Cs = lpeg.C, lpeg.Ct, lpeg.Cs 14 | local quote_sql_str = ngx.quote_sql_str 15 | 16 | local _T = {} 17 | 18 | 19 | local function isqb(tbl) 20 | return type(tbl) == 'table' and tbl._type == 'query' 21 | end 22 | 23 | 24 | 25 | local function quote_var(val) 26 | local typ = type(val) 27 | 28 | if typ == 'boolean' then 29 | return val and 1 or 0 30 | elseif typ == 'string' then 31 | return quote_sql_str(val) 32 | elseif typ == 'number' then 33 | return val 34 | elseif typ == 'nil' then 35 | return "NULL" 36 | elseif typ == 'table' then 37 | if val._type then 38 | return tostring(val) 39 | end 40 | return table_concat(fun.map(quote_var, val), ', ') 41 | else 42 | return tostring(val) 43 | end 44 | end 45 | 46 | local expr = function(str) 47 | local expression = str 48 | return setmetatable({ }, { 49 | __tostring = function(tbl) 50 | return expression 51 | end; 52 | __index = function(tbl, key) 53 | if key == '_type' then 54 | return 'expr' 55 | end 56 | end; 57 | __newindex = function(tbl, key, val) 58 | error('no new value allowed') 59 | end 60 | }) 61 | end 62 | 63 | local build_cond = function(condition, params) 64 | if not params then params = {} end 65 | 66 | local replace_param = function(args) 67 | local counter = { 0 } 68 | local typ = type(args) 69 | return function(func) 70 | return function(cap) 71 | counter[1] = counter[1] + 1 72 | return func(args[counter[1]]) 73 | end 74 | end 75 | end 76 | 77 | local repl = replace_param(params) 78 | 79 | -- table 80 | local parr = P'?t'/repl(function(arg) 81 | if type(arg) ~= 'table' then 82 | arg = { arg } 83 | end 84 | 85 | return quote_var(arg) 86 | end) 87 | local pbool = P'?b'/repl(function(arg) return arg and 1 or 0 end) 88 | local porig = P'?e'/repl(tostring) 89 | -- local pgrp = P'?p'/repl(function(arg) return '('.. tostring(arg) ..')' end) 90 | local pnum = P'?d'/repl(tonumber) 91 | local pnil = P'?n'/repl(function(arg) return arg and 'NOT NULL' or 'NULL' end) 92 | local pstr = P'?s'/repl(quote_sql_str) 93 | local pany = P'?'/repl(quote_var) 94 | 95 | 96 | local patt = Cs((porig + parr + pnum + pstr + pbool + pnil + pany + 1)^0) 97 | local cond = '(' .. patt:match(condition) .. ')' 98 | 99 | 100 | return cond 101 | 102 | end 103 | 104 | 105 | _T.exec = function(self) 106 | return self._db.query(self:build()) 107 | end 108 | 109 | _T.from = function(self, tname, alias) 110 | if isqb(tname) then 111 | if alias then tname = tname:as(alias) end 112 | self._from = tname:build() 113 | else 114 | if alias then tname = tname .. ' ' .. alias end 115 | self._from = self._db.escape_identity(tname) 116 | end 117 | 118 | return self 119 | end 120 | 121 | _T.build_where = function(self, cond, params) 122 | cond = self._db.escape_identity(cond) 123 | return build_cond(cond, params) 124 | end 125 | 126 | _T.select = function(self, fields) 127 | self._select = self._db.escape_identity(fields) 128 | return self 129 | end 130 | 131 | 132 | local function add_cond(self, field, op, cond, params) 133 | if not params then params = {} end 134 | if type(self[field]) ~= 'table' then self[field] = {} end 135 | 136 | table_insert(self[field], { op, cond, params }) 137 | end 138 | 139 | _T.where = function(self, condition, ...) 140 | return self:and_where(condition, ...) 141 | end 142 | 143 | _T.or_where = function(self, condition, ...) 144 | add_cond(self, '_where', 'OR', condition, {...}) 145 | return self 146 | end 147 | 148 | _T.and_where = function(self, condition, ...) 149 | add_cond(self, '_where', 'AND', condition, {...}) 150 | return self 151 | end 152 | 153 | _T.having = function(self, condition, ...) 154 | return self:and_having(condition, ...) 155 | end 156 | 157 | _T.and_having = function(self, condition, ...) 158 | add_cond(self, '_having', 'AND', condition, {...}) 159 | return self 160 | end 161 | 162 | _T.or_having = function(self, condition, ...) 163 | add_cond(self, '_having', 'OR', condition, {...}) 164 | return self 165 | end 166 | 167 | _T.join = function(self, tbl, mode, cond, param) 168 | local cond = build_cond(self._db.escape_identity(cond), param) 169 | if not self._join then self._join = '' end 170 | self._join = table_concat({self._join, mode, 'JOIN', self._db.escape_identity(tbl), 'ON', cond}, ' ') 171 | return self 172 | end 173 | 174 | _T.left_join = function(self, tbl, cond, ...) 175 | return self:join(tbl, 'LEFT', cond, {...}) 176 | end 177 | 178 | _T.right_join = function(self, tbl, cond, ...) 179 | return self:join(tbl, 'RIGHT', cond, {...}) 180 | end 181 | 182 | _T.inner_join = function(self, tbl, cond, ...) 183 | return self:join(tbl, 'INNER', cond, {...}) 184 | end 185 | 186 | _T.group_by = function(self, ...) 187 | self._group_by = false 188 | 189 | local args = { ... } 190 | if #args > 0 then 191 | self._group_by = fun.reduce(function(k, v, acc) 192 | if not acc then 193 | return self._db.escape_identity(v) 194 | else 195 | return acc .. ', ' .. self._db.escape_identity(v) 196 | end 197 | end, false, args) 198 | end 199 | return self 200 | end 201 | 202 | _T.order_by = function(self, ...) 203 | self._order_by = false 204 | 205 | local args = { ... } 206 | if #args > 0 then 207 | self._order_by = fun.reduce(function(k, v, acc) 208 | if not acc then 209 | return self._db.escape_identity(v) 210 | else 211 | return acc .. ', ' .. self._db.escape_identity(v) 212 | end 213 | end, false, args) 214 | end 215 | return self 216 | end 217 | 218 | _T.limit = function(self, arg) 219 | self._limit = tonumber(arg) 220 | return self 221 | end 222 | 223 | _T.offset = function(self, arg) 224 | self._offset = tonumber(arg) 225 | 226 | if self._offset and not self._limit then 227 | self._limit = '18446744073709551615'; 228 | end 229 | return self 230 | end 231 | 232 | _T.as = function(self, as) 233 | self._alias = as 234 | return self 235 | end 236 | 237 | _T.set = function(self, key, val) 238 | self._set = self._set or { } 239 | if type(key) ~= 'table' then 240 | key = { [key] = val } 241 | end 242 | 243 | for k, v in pairs(key) do 244 | self._set[k] = v 245 | end 246 | 247 | return self 248 | end 249 | 250 | _T.values = function(self, vals) 251 | self._values = self._values or { } 252 | for k, v in pairs(vals) do 253 | self._values[k] = v 254 | end 255 | 256 | return self 257 | end 258 | 259 | 260 | _T.delete = function(self, table_name) 261 | self._state = 'delete' 262 | if table_name then 263 | self:from(table_name) 264 | end 265 | return self 266 | end 267 | 268 | _T.update = function(self, table_name) 269 | self._state = 'update' 270 | if table_name then 271 | self:from(table_name) 272 | end 273 | return self 274 | end 275 | 276 | _T.insert = function(self, table_name) 277 | self._state = 'insert' 278 | if table_name then 279 | self:from(table_name) 280 | end 281 | return self 282 | end 283 | 284 | _T.for_update = function(self) 285 | self._for_update = 'FOR UPDATE' 286 | return self 287 | end 288 | 289 | 290 | _T.build = function(self, ...) 291 | local ctx = self._state 292 | 293 | local _make = function(fields) 294 | if not fields then return end 295 | 296 | return fun.reduce(function(k, v, acc) 297 | local tmp = self:build_where(v[2], v[3]) 298 | if strlen(acc) > 0 then 299 | return table_concat({acc, v[1], tmp}, ' ') 300 | else 301 | return tmp 302 | end 303 | end, '', fields) 304 | end 305 | 306 | local _concat = function(f) 307 | if f[2] and strlen(f[2]) > 0 then 308 | return f[1] .. ' ' .. f[2] 309 | end 310 | return nil 311 | end 312 | 313 | local concat = fun.curry(fun.map, _concat) 314 | 315 | local builders = { 316 | select = function() 317 | local sql = table_concat(concat{ 318 | {'SELECT', self._select}, 319 | {'FROM', self._from}, 320 | {'', self._join }, -- join 321 | {'WHERE', _make(self._where)}, 322 | {'GROUP BY', self._group_by}, 323 | {'HAVING', _make(self._having)}, 324 | {'ORDER BY', self._order_by}, 325 | {'LIMIT', self._limit }, 326 | {'OFFSET', self._offset }, 327 | {'', self._for_update} -- for update 328 | }, " ") 329 | 330 | if self._alias then 331 | sql = '(' .. sql ..') AS ' .. self._alias 332 | end 333 | 334 | return sql 335 | end; 336 | 337 | delete = function() 338 | return table_concat(concat{ 339 | { 'DELETE FROM', self._from }, 340 | { 'WHERE', _make(self._where) } 341 | }, ' ') 342 | end; 343 | 344 | update = function() 345 | return table_concat(concat{ 346 | { 'UPDATE', self._from }, 347 | { 'SET', fun.reduce(function(k, v, acc) 348 | local where = '' 349 | if type(k) == 'number' then 350 | where = self._db.escape_identity(v) 351 | else 352 | where = self._db.escape_identity(k) .. '=' .. quote_var(v) 353 | end 354 | if not acc then return where end 355 | return acc .. ', ' .. where 356 | end, nil, self._set)}, 357 | { 'WHERE', _make(self._where) } 358 | }, ' ') 359 | end; 360 | 361 | insert = function() 362 | -- insert into `table` (f1, f2) values ( v1, v2) 363 | local keys = fun.table_keys(self._values) 364 | local vals = fun.map(function(v, k) 365 | return quote_var(self._values[v]) 366 | end, keys) 367 | 368 | return table_concat(concat{ 369 | { 'INSERT INTO', self._from }, 370 | { '', '('.. self._db.escape_identity(table_concat(keys, ', ')) .. ')'}, 371 | { 'VALUES', '(' .. table_concat(vals, ', ') .. ')' } 372 | }, ' ') 373 | 374 | end; 375 | } 376 | 377 | return builders[ctx](...) 378 | 379 | end 380 | 381 | local function create_query(db) 382 | 383 | local qb = { 384 | _db = db, 385 | _state = 'select', 386 | _type = 'query', 387 | 388 | _from = false, 389 | _select = '*', 390 | _join = false, 391 | _where = false, 392 | _using_index = false, 393 | _having = false, 394 | _group_by = false, 395 | _limit = false, 396 | _offset = false, 397 | _alias = false, 398 | _order_by = false, 399 | _set = false, 400 | _values = false, 401 | _for_update = false, 402 | } 403 | 404 | local mt = { 405 | __index = _T, 406 | __newindex = function(tbl, key, val) 407 | error('no new value allowed') 408 | end; 409 | __tostring = function(self) 410 | return self:build() 411 | end; 412 | __call = function(self, ...) 413 | return self:exec(...) 414 | end 415 | } 416 | 417 | return setmetatable(qb, mt) 418 | end 419 | 420 | 421 | return { 422 | expr = expr, 423 | create = create_query, 424 | } 425 | --------------------------------------------------------------------------------