local _M = {} local field_attrs = {"field", "type", "collation", "null", "key", "default", "extra", "comment", "the_index"} local function showcolumn(ctx, tbl) local ret = {} local sqlret = assert(ctx.query(string.format("show full columns from %s", tbl))) for k, v in ipairs(sqlret) do local c = {} for _k, _v in pairs(v) do c[string.lower(_k)] = _v end c.the_index = tostring(k) for _, key in ipairs(field_attrs) do if c[key] == nil then c[key] = "__NULL__" end end table.insert(ret, c) end return ret end local function showtables(ctx) local ret = {} local sqlret = assert(ctx.query("show tables;")) for _, v in ipairs(sqlret) do for _, value in pairs(v) do table.insert(ret, value) end end return ret end local function showcreatetable(ctx, tbl) local sqlret = assert(ctx.query(string.format("show create table %s", tbl))) local str = sqlret[1]["Create Table"] str = string.gsub(str, " AUTO_INCREMENT=%d*", "") str = string.gsub(str, " USING BTREE", "USING HASH") str = string.gsub(str, " ROW_FORMAT=DYNAMIC", "") str = string.gsub(str, " ROW_FORMAT=FIXED", "") str = string.gsub(str, " ROW_FORMAT=COMPACT", "") str = string.gsub(str, "ENGINE=%w*", "ENGINE=InnoDB") return str end local function showindex(ctx, tbl) local ret = {} local sqlret = assert(ctx.query(string.format("show index from %s", tbl))) for _, v in ipairs(sqlret) do local c = {} for _k, _v in pairs(v) do c[string.lower(_k)] = _v end table.insert(ret, c) end return ret end local function showcreateprocedure(ctx, proc) local sqlret = assert(ctx.query(string.format("show create procedure %s", proc))) local str = sqlret[1]["Create Procedure"] str = string.gsub(str, "CREATE(.*)PROCEDURE", "CREATE PROCEDURE") -- str=string.format("DROP PROCEDURE IF EXISTS `%s`;\nDELIMITER $$\n%s\n$$\nDELIMITER ;",proc,str) return str end local function showcreatefunction(ctx, proc) local sqlret = assert(ctx.query(string.format("show create function %s", proc))) local str = sqlret[1]["Create Function"] str = string.gsub(str, "CREATE(.*)FUNCTION", "CREATE FUNCTION") -- str=string.format("DROP PROCEDURE IF EXISTS `%s`;\nDELIMITER $$\n%s\n$$\nDELIMITER ;",proc,str) return str end local function showdatabase(ctx) local sqlret = assert(ctx.query("select database()")) return sqlret[1]["database()"] end local function showprocedures(ctx) local dbname = showdatabase(ctx) local sqlret = assert(ctx.query(string.format("select name,type from mysql.proc where db='%s'", dbname))) local ret, func = {}, {} for _, v in pairs(sqlret) do local name, type = v.name, v.type if type == 'PROCEDURE' then table.insert(ret, name); else table.insert(func, name) end end return ret, func end function _M.newctx(ret, opt) ret.query = assert(opt.query) return ret end function _M.load(ctx) ctx.table_list = showtables(ctx) ctx.table = setmetatable({}, { __index = function(t, k) local r = showcolumn(ctx, k) t[k] = r return r end, }) ctx.tablecreate = setmetatable({}, { __index = function(t, k) local r = showcreatetable(ctx, k) t[k] = r return r end, }) ctx.index = setmetatable({}, { __index = function(t, k) local r = showindex(ctx, k) t[k] = r return r end, }) local proc, func = showprocedures(ctx) ctx.proc_list = proc ctx.proc = setmetatable({}, { __index = function(t, k) local r = showcreateprocedure(ctx, k) t[k] = r return r end, }) ctx.func_list = func ctx.func = setmetatable({}, { __index = function(t, k) local r = showcreatefunction(ctx, k) t[k] = r return r end, }) return ctx end local function array2dict(array) local d = {} for k, v in ipairs(array) do d[v] = k end return d end local function fieldfind(tbl, k) for _, v in pairs(tbl) do if v.field == k then return v end end end local function markfield(set, tbl, v) local null = "" local default = "" if v.null == "NO" then null = "NOT NULL" end if v.default == "__NULL__" then if v.null ~= "NO" then default = "DEFAULT NULL" end else default = string.format("DEFAULT '%s'", v.default) end local collate = "" if v.collation ~= "" and v.collation ~= "__NULL__" then collate = string.format("COLLATE '%s'", v.collation) end local cmt = "" if v.comment ~= "" then cmt = string.format("COMMENT '%s'", v.comment) end local pos = 'FIRST' if tonumber(v.the_index) > 1 then pos = string.format("ALTER `%s`", set[v.the_index - 1].field) end return null, default, cmt, collate, pos end local function make_changefield(set, tbl, v) local null, default, cmt, collate, pos = markfield(set, tbl, v) return string.format("alter table `%s` change column `%s` `%s` %s %s %s %s %s %s %s", tbl, v.field, v.field, string.lower(v.type), null, default, cmt, collate, string.lower(v.extra or ""), pos) end local function make_addfield(set, tbl, v) local null, default, cmt, collate, pos = markfield(set, tbl, v) return string.format("alter table `%s` add column `%s` %s %s %s %s %s %s %s", tbl, v.field, string.lower(v.type), null, default, cmt, collate, string.lower(v.extra or ""), pos) end local function tablefield_compare(l, r) if l == r then return true end assert(l.field == r.field) for _, k in ipairs(field_attrs) do if k ~= "key" and l[k] ~= r[k] then return false end end return true end local function fields_re_index(tbl) for k, v in ipairs(tbl) do v.the_index = tostring(k) end end local function compare_fields(ret, name, lfields, rfields) while true do local over = true for k, lfield in ipairs(lfields) do local rfield = fieldfind(rfields, lfield.field) if not rfield then table.insert(ret, string.format("alter table `%s` drop column `%s`", name, lfield.field)) table.remove(lfields, k) over = false break end end fields_re_index(lfields) if over then break end end while true do local over = true for k, rfield in ipairs(rfields) do local lfield = fieldfind(lfields, rfield.field) if not lfield then table.insert(ret, make_addfield(lfields, k, rfield)) elseif not tablefield_compare(lfield, rfield) then table.insert(ret, make_changefield(lfields, k, rfield)) end end fields_re_index(lfields) if over then break end end end local function gensql(left, right) local sdict = array2dict(left.table_list) local cdict = array2dict(right.table_list) local ret = {} for k in pairs(sdict) do if not cdict[k] then table.insert(ret, (string.format("drop table if exists `%s`", k))) else local stbl = left.table[k] local ctbl = right.table[k] compare_fields(ret, k, stbl, ctbl) end end for k in pairs(cdict) do if not sdict[k] then table.insert(ret, right.tablecreate[k]) end end local sdict = array2dict(left.proc_list) local cdict = array2dict(right.proc_list) for k in pairs(sdict) do if not cdict[k] then table.insert(ret, (string.format("drop procedure if exists `%s`", k))) else local sproc = left.proc[k] local cproc = right.proc[k] if sproc ~= cproc then table.insert(ret, (string.format("drop procedure if exists `%s`", k))) table.insert(ret, cproc) end end end for k in pairs(cdict) do if not sdict[k] then table.insert(ret, right.proc[k]) end end local sdict = array2dict(left.func_list) local cdict = array2dict(right.func_list) for k in pairs(sdict) do if not cdict[k] then table.insert(ret, (string.format("drop function if exists `%s`", k))) else local sfunc = left.func[k] local cfunc = right.func[k] if sfunc ~= cfunc then table.insert(ret, (string.format("drop function if exists `%s`", k))) table.insert(ret, cfunc) end end end for k in pairs(cdict) do if not sdict[k] then table.insert(ret, right.func[k]) end end return ret end function _M.save(ctx) local left = _M.newctx({}, ctx) _M.load(left) local ret = gensql(left, ctx) for _, v in ipairs(ret) do left.query(v) end end return _M