jbe/bsw@0: #!/usr/bin/env lua jbe/bsw@0: jbe/bsw@0: jbe/bsw@0: --------------------------- jbe/bsw@0: -- module initialization -- jbe/bsw@0: --------------------------- jbe/bsw@0: jbe/bsw@0: local _G = _G jbe/bsw@0: local _VERSION = _VERSION jbe/bsw@0: local assert = assert jbe/bsw@0: local collectgarbage = collectgarbage jbe/bsw@0: local dofile = dofile jbe/bsw@0: local error = error jbe/bsw@0: local getfenv = getfenv jbe/bsw@0: local getmetatable = getmetatable jbe/bsw@0: local ipairs = ipairs jbe/bsw@0: local load = load jbe/bsw@0: local loadfile = loadfile jbe/bsw@0: local loadstring = loadstring jbe/bsw@0: local next = next jbe/bsw@0: local pairs = pairs jbe/bsw@0: local pcall = pcall jbe/bsw@0: local print = print jbe/bsw@0: local rawequal = rawequal jbe/bsw@0: local rawget = rawget jbe/bsw@0: local rawset = rawset jbe/bsw@0: local select = select jbe/bsw@0: local setfenv = setfenv jbe/bsw@0: local setmetatable = setmetatable jbe/bsw@0: local tonumber = tonumber jbe/bsw@0: local tostring = tostring jbe/bsw@0: local type = type jbe/bsw@0: local unpack = unpack jbe/bsw@0: local xpcall = xpcall jbe/bsw@0: jbe/bsw@0: local coroutine = coroutine jbe/bsw@0: local io = io jbe/bsw@0: local math = math jbe/bsw@0: local os = os jbe/bsw@0: local string = string jbe/bsw@0: local table = table jbe/bsw@0: jbe/bsw@0: local add = table.insert jbe/bsw@0: jbe/bsw@0: _G[...] = require("mondelefant_native") jbe/bsw@0: module(...) jbe/bsw@0: jbe/bsw@0: jbe/bsw@0: jbe/bsw@0: --------------- jbe/bsw@0: -- selectors -- jbe/bsw@0: --------------- jbe/bsw@0: jbe/bsw@0: selector_metatable = {} jbe/bsw@0: selector_prototype = {} jbe/bsw@0: selector_metatable.__index = selector_prototype jbe/bsw@0: jbe/bsw@0: local function init_selector(self, db_conn) jbe/bsw@0: self._db_conn = db_conn jbe/bsw@0: self._mode = "list" jbe/bsw@0: self._fields = { sep = ", " } jbe/bsw@0: self._distinct = false jbe/bsw@0: self._distinct_on = {sep = ", ", expression} jbe/bsw@0: self._from = { sep = " " } jbe/bsw@0: self._where = { sep = " AND " } jbe/bsw@0: self._group_by = { sep = ", " } jbe/bsw@0: self._having = { sep = " AND " } jbe/bsw@0: self._combine = { sep = " " } jbe/bsw@0: self._order_by = { sep = ", " } jbe/bsw@0: self._limit = nil jbe/bsw@0: self._offset = nil jbe/bsw@0: --[[ jbe/bsw@0: self._lock = nil jbe/bsw@0: self._lock_tables = { sep = ", " } jbe/bsw@0: --]] jbe/bsw@0: self._class = nil jbe/bsw@0: self._attach = nil jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function connection_prototype:new_selector() jbe/bsw@0: return init_selector(setmetatable({}, selector_metatable), self) jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:get_db_conn() jbe/bsw@0: return self._db_conn jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: -- TODO: selector clone? jbe/bsw@0: jbe/bsw@0: function selector_prototype:single_object_mode() jbe/bsw@0: self._mode = "object" jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:optional_object_mode() jbe/bsw@0: self._mode = "opt_object" jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:empty_list_mode() jbe/bsw@0: self._mode = "empty_list" jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:add_distinct_on(expression) jbe/bsw@0: if self._distinct then jbe/bsw@0: error("Can not combine DISTINCT with DISTINCT ON.") jbe/bsw@0: end jbe/bsw@0: add(self._distinct_on, expression) jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:set_distinct() jbe/bsw@0: if #self._distinct_on > 0 then jbe/bsw@0: error("Can not combine DISTINCT with DISTINCT ON.") jbe/bsw@0: end jbe/bsw@0: self._distinct = true jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:add_from(expression, alias, condition) jbe/bsw@0: local first = (#self._from == 0) jbe/bsw@0: if not first then jbe/bsw@0: if condition then jbe/bsw@0: add(self._from, "INNER JOIN") jbe/bsw@0: else jbe/bsw@0: add(self._from, "CROSS JOIN") jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: if getmetatable(expression) == selector_metatable then jbe/bsw@0: if alias then jbe/bsw@0: add(self._from, {'($) AS "$"', {expression}, {alias}}) jbe/bsw@0: else jbe/bsw@0: add(self._from, {'($) AS "subquery"', {expression}}) jbe/bsw@0: end jbe/bsw@0: else jbe/bsw@0: if alias then jbe/bsw@0: add(self._from, {'$ AS "$"', {expression}, {alias}}) jbe/bsw@0: else jbe/bsw@0: add(self._from, expression) jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: if condition then jbe/bsw@0: if first then jbe/bsw@0: self:condition(condition) jbe/bsw@0: else jbe/bsw@0: add(self._from, "ON") jbe/bsw@0: add(self._from, condition) jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:add_where(expression) jbe/bsw@0: add(self._where, expression) jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:add_group_by(expression) jbe/bsw@0: add(self._group_by, expression) jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:add_having(expression) jbe/bsw@0: add(self._having, expression) jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:add_combine(expression) jbe/bsw@0: add(self._combine, expression) jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:add_order_by(expression) jbe/bsw@0: add(self._order_by, expression) jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:limit(count) jbe/bsw@0: if type(count) ~= "number" or count % 1 ~= 0 then jbe/bsw@0: error("LIMIT must be an integer.") jbe/bsw@0: end jbe/bsw@0: self._limit = count jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:offset(count) jbe/bsw@0: if type(count) ~= "number" or count % 1 ~= 0 then jbe/bsw@0: error("OFFSET must be an integer.") jbe/bsw@0: end jbe/bsw@0: self._offset = count jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:reset_fields() jbe/bsw@0: for idx in ipairs(self._fields) do jbe/bsw@0: self._fields[idx] = nil jbe/bsw@0: end jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:add_field(expression, alias, options) jbe/bsw@0: if alias then jbe/bsw@0: add(self._fields, {'$ AS "$"', {expression}, {alias}}) jbe/bsw@0: else jbe/bsw@0: add(self._fields, expression) jbe/bsw@0: end jbe/bsw@0: if options then jbe/bsw@0: for i, option in ipairs(options) do jbe/bsw@0: if option == "distinct" then jbe/bsw@0: if alias then jbe/bsw@0: self:add_distinct_on('"' .. alias .. '"') jbe/bsw@0: else jbe/bsw@0: self:add_distinct_on(expression) jbe/bsw@0: end jbe/bsw@0: elseif option == "grouped" then jbe/bsw@0: if alias then jbe/bsw@0: self:add_group_by('"' .. alias .. '"') jbe/bsw@0: else jbe/bsw@0: self:add_group_by(expression) jbe/bsw@0: end jbe/bsw@0: else jbe/bsw@0: error("Unknown option '" .. option .. "' to add_field method.") jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:join(...) -- NOTE: alias for add_from jbe/bsw@0: return self:add_from(...) jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:from(expression, alias, condition) jbe/bsw@0: if #self._from > 0 then jbe/bsw@0: error("From-clause already existing (hint: try join).") jbe/bsw@0: end jbe/bsw@0: return self:join(expression, alias, condition) jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:left_join(expression, alias, condition) jbe/bsw@0: local first = (#self._from == 0) jbe/bsw@0: if not first then jbe/bsw@0: add(self._from, "LEFT OUTER JOIN") jbe/bsw@0: end jbe/bsw@0: if alias then jbe/bsw@0: add(self._from, {'$ AS "$"', {expression}, {alias}}) jbe/bsw@0: else jbe/bsw@0: add(self._from, expression) jbe/bsw@0: end jbe/bsw@0: if condition then jbe/bsw@0: if first then jbe/bsw@0: self:condition(condition) jbe/bsw@0: else jbe/bsw@0: add(self._from, "ON") jbe/bsw@0: add(self._from, condition) jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:union(expression) jbe/bsw@0: self:add_combine{"UNION $", {expression}} jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:union_all(expression) jbe/bsw@0: self:add_combine{"UNION ALL $", {expression}} jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:intersect(expression) jbe/bsw@0: self:add_combine{"INTERSECT $", {expression}} jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:intersect_all(expression) jbe/bsw@0: self:add_combine{"INTERSECT ALL $", {expression}} jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:except(expression) jbe/bsw@0: self:add_combine{"EXCEPT $", {expression}} jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:except_all(expression) jbe/bsw@0: self:add_combine{"EXCEPT ALL $", {expression}} jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:set_class(class) jbe/bsw@0: self._class = class jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:attach(mode, data2, field1, field2, ref1, ref2) jbe/bsw@0: self._attach = { jbe/bsw@0: mode = mode, jbe/bsw@0: data2 = data2, jbe/bsw@0: field1 = field1, jbe/bsw@0: field2 = field2, jbe/bsw@0: ref1 = ref1, jbe/bsw@0: ref2 = ref2 jbe/bsw@0: } jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: -- TODO: many-to-many relations jbe/bsw@0: jbe/bsw@0: function selector_metatable:__tostring() jbe/bsw@0: local parts = {sep = " "} jbe/bsw@0: add(parts, "SELECT") jbe/bsw@0: if self._distinct then jbe/bsw@0: add(parts, "DISTINCT") jbe/bsw@0: elseif #self._distinct_on > 0 then jbe/bsw@0: add(parts, {"DISTINCT ON ($)", self._distinct_on}) jbe/bsw@0: end jbe/bsw@0: add(parts, {"$", self._fields}) jbe/bsw@0: if #self._from > 0 then jbe/bsw@0: add(parts, {"FROM $", self._from}) jbe/bsw@0: end jbe/bsw@0: if #self._mode == "empty_list" then jbe/bsw@0: add(parts, "WHERE FALSE") jbe/bsw@0: elseif #self._where > 0 then jbe/bsw@0: add(parts, {"WHERE $", self._where}) jbe/bsw@0: end jbe/bsw@0: if #self._group_by > 0 then jbe/bsw@0: add(parts, {"GROUP BY $", self._group_by}) jbe/bsw@0: end jbe/bsw@0: if #self._having > 0 then jbe/bsw@0: add(parts, {"HAVING $", self._having}) jbe/bsw@0: end jbe/bsw@0: for i, v in ipairs(self._combine) do jbe/bsw@0: add(parts, v) jbe/bsw@0: end jbe/bsw@0: if #self._order_by > 0 then jbe/bsw@0: add(parts, {"ORDER BY $", self._order_by}) jbe/bsw@0: end jbe/bsw@0: if self._mode == "empty_list" then jbe/bsw@0: add(parts, "LIMIT 0") jbe/bsw@0: elseif self._mode ~= "list" then jbe/bsw@0: add(parts, "LIMIT 1") jbe/bsw@0: elseif self._limit then jbe/bsw@0: add(parts, "LIMIT " .. self._limit) jbe/bsw@0: end jbe/bsw@0: if self._offset then jbe/bsw@0: add(parts, "OFFSET " .. self._offset) jbe/bsw@0: end jbe/bsw@0: return self._db_conn:assemble_command{"$", parts} jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:try_exec() jbe/bsw@0: if self._mode == "empty_list" then jbe/bsw@0: if self._class then jbe/bsw@0: return nil, self._class:create_list() jbe/bsw@0: else jbe/bsw@0: return nil, self._db_conn:create_list() jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: local db_error, db_result = self._db_conn:try_query(self, self._mode) jbe/bsw@0: if db_error then jbe/bsw@0: return db_error jbe/bsw@0: elseif db_result then jbe/bsw@0: if self._class then set_class(db_result, self._class) end jbe/bsw@0: if self._attach then jbe/bsw@0: attach( jbe/bsw@0: self._attach.mode, jbe/bsw@0: db_result, jbe/bsw@0: self._attach.data2, jbe/bsw@0: self._attach.field1, jbe/bsw@0: self._attach.field2, jbe/bsw@0: self._attach.ref1, jbe/bsw@0: self._attach.ref2 jbe/bsw@0: ) jbe/bsw@0: end jbe/bsw@0: return nil, db_result jbe/bsw@0: else jbe/bsw@0: return nil jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function selector_prototype:exec() jbe/bsw@0: local db_error, result = self:try_exec() jbe/bsw@0: if db_error then jbe/bsw@0: db_error:escalate() jbe/bsw@0: else jbe/bsw@0: return result jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: jbe/bsw@0: jbe/bsw@0: ----------------- jbe/bsw@0: -- attachments -- jbe/bsw@0: ----------------- jbe/bsw@0: jbe/bsw@0: local function attach_key(row, fields) jbe/bsw@0: local t = type(fields) jbe/bsw@0: if t == "string" then jbe/bsw@0: return tostring(row[fields]) jbe/bsw@0: elseif t == "table" then jbe/bsw@0: local r = {} jbe/bsw@0: for idx, field in ipairs(fields) do jbe/bsw@0: r[idx] = string.format("%q", row[field]) jbe/bsw@0: end jbe/bsw@0: return table.concat(r) jbe/bsw@0: else jbe/bsw@0: error("Field information for 'mondelefant.attach' is neither a string nor a table.") jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function attach(mode, data1, data2, key1, key2, ref1, ref2) jbe/bsw@0: local many1, many2 jbe/bsw@0: if mode == "11" then jbe/bsw@0: many1 = false jbe/bsw@0: many2 = false jbe/bsw@0: elseif mode == "1m" then jbe/bsw@0: many1 = false jbe/bsw@0: many2 = true jbe/bsw@0: elseif mode == "m1" then jbe/bsw@0: many1 = true jbe/bsw@0: many2 = false jbe/bsw@0: elseif mode == "mm" then jbe/bsw@0: many1 = true jbe/bsw@0: many2 = true jbe/bsw@0: else jbe/bsw@0: error("Unknown mode specified for 'mondelefant.attach'.") jbe/bsw@0: end jbe/bsw@0: local list1, list2 jbe/bsw@0: if data1._type == "object" then jbe/bsw@0: list1 = { data1 } jbe/bsw@0: elseif data1._type == "list" then jbe/bsw@0: list1 = data1 jbe/bsw@0: else jbe/bsw@0: error("First result data given to 'mondelefant.attach' is invalid.") jbe/bsw@0: end jbe/bsw@0: if data2._type == "object" then jbe/bsw@0: list2 = { data2 } jbe/bsw@0: elseif data2._type == "list" then jbe/bsw@0: list2 = data2 jbe/bsw@0: else jbe/bsw@0: error("Second result data given to 'mondelefant.attach' is invalid.") jbe/bsw@0: end jbe/bsw@0: local hash1 = {} jbe/bsw@0: local hash2 = {} jbe/bsw@0: if ref2 then jbe/bsw@0: for i, row in ipairs(list1) do jbe/bsw@0: local key = attach_key(row, key1) jbe/bsw@0: local list = hash1[key] jbe/bsw@0: if not list then list = {}; hash1[key] = list end jbe/bsw@0: list[#list + 1] = row jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: if ref1 then jbe/bsw@0: for i, row in ipairs(list2) do jbe/bsw@0: local key = attach_key(row, key2) jbe/bsw@0: local list = hash2[key] jbe/bsw@0: if not list then list = {}; hash2[key] = list end jbe/bsw@0: list[#list + 1] = row jbe/bsw@0: end jbe/bsw@0: for i, row in ipairs(list1) do jbe/bsw@0: local key = attach_key(row, key1) jbe/bsw@0: local matching_rows = hash2[key] jbe/bsw@0: if many2 then jbe/bsw@0: local list = data2._connection:create_list(matching_rows) jbe/bsw@0: list._class = data2._class jbe/bsw@0: row._ref[ref1] = list jbe/bsw@0: elseif matching_rows and #matching_rows == 1 then jbe/bsw@0: row._ref[ref1] = matching_rows[1] jbe/bsw@0: else jbe/bsw@0: row._ref[ref1] = false jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: if ref2 then jbe/bsw@0: for i, row in ipairs(list2) do jbe/bsw@0: local key = attach_key(row, key2) jbe/bsw@0: local matching_rows = hash1[key] jbe/bsw@0: if many1 then jbe/bsw@0: local list = data1._connection:create_list(matching_rows) jbe/bsw@0: list._class = data1._class jbe/bsw@0: row._ref[ref2] = list jbe/bsw@0: elseif matching_rows and #matching_rows == 1 then jbe/bsw@0: row._ref[ref2] = matching_rows[1] jbe/bsw@0: else jbe/bsw@0: row._ref[ref2] = false jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: jbe/bsw@0: jbe/bsw@0: ------------------ jbe/bsw@0: -- model system -- jbe/bsw@0: ------------------ jbe/bsw@0: jbe/bsw@0: class_prototype.primary_key = "id" jbe/bsw@0: jbe/bsw@0: function class_prototype:get_db_conn() jbe/bsw@0: error( jbe/bsw@0: "Method mondelefant class(_prototype):get_db_conn() " .. jbe/bsw@0: "has to be implemented." jbe/bsw@0: ) jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype:get_qualified_table() jbe/bsw@0: if not self.table then error "Table unknown." end jbe/bsw@0: if self.schema then jbe/bsw@0: return '"' .. self.schema .. '"."' .. self.table .. '"' jbe/bsw@0: else jbe/bsw@0: return '"' .. self.table .. '"' jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype:get_qualified_table_literal() jbe/bsw@0: if not self.table then error "Table unknown." end jbe/bsw@0: if self.schema then jbe/bsw@0: return self.schema .. '.' .. self.table jbe/bsw@0: else jbe/bsw@0: return self.table jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype:get_primary_key_list() jbe/bsw@0: local primary_key = self.primary_key jbe/bsw@0: if type(primary_key) == "string" then jbe/bsw@0: return {primary_key} jbe/bsw@0: else jbe/bsw@0: return primary_key jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype:get_columns() jbe/bsw@0: if self._columns then jbe/bsw@0: return self._columns jbe/bsw@0: end jbe/bsw@0: local selector = self:get_db_conn():new_selector() jbe/bsw@0: selector:set_class(self) jbe/bsw@0: selector:from(self:get_qualified_table()) jbe/bsw@0: selector:add_field("*") jbe/bsw@0: selector:add_where("FALSE") jbe/bsw@0: local db_result = selector:exec() jbe/bsw@0: local connection = db_result._connection jbe/bsw@0: local columns = {} jbe/bsw@0: for idx, info in ipairs(db_result._column_info) do jbe/bsw@0: local key = info.field_name jbe/bsw@0: local value = { jbe/bsw@0: name = key, jbe/bsw@0: type = connection.type_mappings[info.type] jbe/bsw@0: } jbe/bsw@0: columns[key] = value jbe/bsw@0: table.insert(columns, value) jbe/bsw@0: end jbe/bsw@0: self._columns = columns jbe/bsw@0: return columns jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype:new_selector(db_conn) jbe/bsw@0: local selector = (db_conn or self:get_db_conn()):new_selector() jbe/bsw@0: selector:set_class(self) jbe/bsw@0: selector:from(self:get_qualified_table()) jbe/bsw@0: selector:add_field(self:get_qualified_table() .. ".*") jbe/bsw@0: return selector jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype:create_list() jbe/bsw@0: local list = self:get_db_conn():create_list() jbe/bsw@0: list._class = self jbe/bsw@0: return list jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype:new() jbe/bsw@0: local object = self:get_db_conn():create_object() jbe/bsw@0: object._class = self jbe/bsw@0: object._new = true jbe/bsw@0: return object jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype.object:try_save() jbe/bsw@0: if not self._class then jbe/bsw@0: error("Cannot save object: No class information available.") jbe/bsw@0: end jbe/bsw@0: local primary_key = self._class:get_primary_key_list() jbe/bsw@0: local primary_key_sql = { sep = ", " } jbe/bsw@0: for idx, value in ipairs(primary_key) do jbe/bsw@0: primary_key_sql[idx] = '"' .. value .. '"' jbe/bsw@0: end jbe/bsw@0: if self._new then jbe/bsw@0: local fields = {sep = ", "} jbe/bsw@0: local values = {sep = ", "} jbe/bsw@0: for key, dummy in pairs(self._dirty or {}) do jbe/bsw@0: add(fields, {'"$"', {key}}) jbe/bsw@0: add(values, {'?', self[key]}) jbe/bsw@0: end jbe/bsw@0: if compat_returning then -- compatibility for PostgreSQL 8.1 jbe/bsw@0: local db_error, db_result1, db_result2 = self._connection:try_query( jbe/bsw@0: { jbe/bsw@0: 'INSERT INTO $ ($) VALUES ($)', jbe/bsw@0: {self._class:get_qualified_table()}, jbe/bsw@0: fields, jbe/bsw@0: values, jbe/bsw@0: primary_key_sql jbe/bsw@0: }, jbe/bsw@0: "list", jbe/bsw@0: { jbe/bsw@0: 'SELECT currval(?)', jbe/bsw@0: self._class.table .. '_id_seq' jbe/bsw@0: }, jbe/bsw@0: "object" jbe/bsw@0: ) jbe/bsw@0: if db_error then jbe/bsw@0: return db_error jbe/bsw@0: end jbe/bsw@0: self.id = db_result2.id jbe/bsw@0: else jbe/bsw@0: local db_error, db_result = self._connection:try_query( jbe/bsw@0: { jbe/bsw@0: 'INSERT INTO $ ($) VALUES ($) RETURNING ($)', jbe/bsw@0: {self._class:get_qualified_table()}, jbe/bsw@0: fields, jbe/bsw@0: values, jbe/bsw@0: primary_key_sql jbe/bsw@0: }, jbe/bsw@0: "object" jbe/bsw@0: ) jbe/bsw@0: if db_error then jbe/bsw@0: return db_error jbe/bsw@0: end jbe/bsw@0: for idx, value in ipairs(primary_key) do jbe/bsw@0: self[value] = db_result[value] jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: self._new = false jbe/bsw@0: else jbe/bsw@0: local command_sets = {sep = ", "} jbe/bsw@0: for key, dummy in pairs(self._dirty or {}) do jbe/bsw@0: add(command_sets, {'"$" = ?', {key}, self[key]}) jbe/bsw@0: end jbe/bsw@0: if #command_sets >= 1 then jbe/bsw@0: local primary_key_compare = {sep = " AND "} jbe/bsw@0: for idx, value in ipairs(primary_key) do jbe/bsw@0: primary_key_compare[idx] = { jbe/bsw@0: "$ = ?", jbe/bsw@0: {'"' .. value .. '"'}, jbe/bsw@0: self[value] jbe/bsw@0: } jbe/bsw@0: end jbe/bsw@0: local db_error = self._connection:try_query{ jbe/bsw@0: 'UPDATE $ SET $ WHERE $', jbe/bsw@0: {self._class:get_qualified_table()}, jbe/bsw@0: command_sets, jbe/bsw@0: primary_key_compare jbe/bsw@0: } jbe/bsw@0: if db_error then jbe/bsw@0: return db_error jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: return nil jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype.object:save() jbe/bsw@0: local db_error = self:try_save() jbe/bsw@0: if db_error then jbe/bsw@0: db_error:escalate() jbe/bsw@0: end jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype.object:try_destroy() jbe/bsw@0: if not self._class then jbe/bsw@0: error("Cannot destroy object: No class information available.") jbe/bsw@0: end jbe/bsw@0: local primary_key = self._class:get_primary_key_list() jbe/bsw@0: local primary_key_compare = {sep = " AND "} jbe/bsw@0: for idx, value in ipairs(primary_key) do jbe/bsw@0: primary_key_compare[idx] = { jbe/bsw@0: "$ = ?", jbe/bsw@0: {'"' .. value .. '"'}, jbe/bsw@0: self[value] jbe/bsw@0: } jbe/bsw@0: end jbe/bsw@0: return self._connection:try_query{ jbe/bsw@0: 'DELETE FROM $ WHERE $', jbe/bsw@0: {self._class:get_qualified_table()}, jbe/bsw@0: primary_key_compare jbe/bsw@0: } jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype.object:destroy() jbe/bsw@0: local db_error = self:try_destroy() jbe/bsw@0: if db_error then jbe/bsw@0: db_error:escalate() jbe/bsw@0: end jbe/bsw@0: return self jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype.list:get_reference_selector( jbe/bsw@0: ref_name, options, ref_alias, back_ref_alias jbe/bsw@0: ) jbe/bsw@0: local ref_info = self._class.references[ref_name] jbe/bsw@0: if not ref_info then jbe/bsw@0: error('Reference with name "' .. ref_name .. '" not found.') jbe/bsw@0: end jbe/bsw@0: local selector = ref_info.selector_generator(self, options or {}) jbe/bsw@0: local mode = ref_info.mode jbe/bsw@0: if mode == "mm" or mode == "1m" then jbe/bsw@0: mode = "m1" jbe/bsw@0: elseif mode == "m1" then jbe/bsw@0: mode = "1m" jbe/bsw@0: end jbe/bsw@0: local ref_alias = ref_alias jbe/bsw@0: if ref_alias == false then jbe/bsw@0: ref_alias = nil jbe/bsw@0: elseif ref_alias == nil then jbe/bsw@0: ref_alias = ref_name jbe/bsw@0: end jbe/bsw@0: local back_ref_alias jbe/bsw@0: if back_ref_alias == false then jbe/bsw@0: back_ref_alias = nil jbe/bsw@0: elseif back_ref_alias == nil then jbe/bsw@0: back_ref_alias = ref_info.back_ref jbe/bsw@0: end jbe/bsw@0: selector:attach( jbe/bsw@0: mode, jbe/bsw@0: self, jbe/bsw@0: ref_info.that_key, ref_info.this_key, jbe/bsw@0: back_ref_alias or ref_info.back_ref, ref_alias or ref_name jbe/bsw@0: ) jbe/bsw@0: return selector jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype.list.load(...) jbe/bsw@0: return class_prototype.list.get_reference_selector(...):exec() jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype.object:get_reference_selector(...) jbe/bsw@0: local list = self._class:create_list() jbe/bsw@0: list[1] = self jbe/bsw@0: return list:get_reference_selector(...) jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: function class_prototype.object.load(...) jbe/bsw@0: return class_prototype.object.get_reference_selector(...):exec() jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: jbe/bsw@0: function class_prototype:add_reference(args) jbe/bsw@0: local selector_generator = args.selector_generator jbe/bsw@0: local mode = args.mode jbe/bsw@0: local to = args.to jbe/bsw@0: local this_key = args.this_key jbe/bsw@0: local that_key = args.that_key jbe/bsw@0: local connected_by_table = args.connected_by_table -- TODO: split to table and schema jbe/bsw@0: local connected_by_this_key = args.connected_by_this_key jbe/bsw@0: local connected_by_that_key = args.connected_by_that_key jbe/bsw@0: local ref = args.ref jbe/bsw@0: local back_ref = args.back_ref jbe/bsw@0: local default_order = args.default_order jbe/bsw@0: local model jbe/bsw@0: local function get_model() jbe/bsw@0: if not model then jbe/bsw@0: if type(to) == "string" then jbe/bsw@0: model = _G jbe/bsw@0: for path_element in string.gmatch(to, "[^.]+") do jbe/bsw@0: model = model[path_element] jbe/bsw@0: end jbe/bsw@0: elseif type(to) == "function" then jbe/bsw@0: model = to() jbe/bsw@0: else jbe/bsw@0: model = to jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: if not model or model == _G then jbe/bsw@0: error("Could not get model for reference.") jbe/bsw@0: end jbe/bsw@0: return model jbe/bsw@0: end jbe/bsw@0: self.references[ref] = { jbe/bsw@0: mode = mode, jbe/bsw@0: this_key = this_key, jbe/bsw@0: that_key = connected_by_table and "mm_ref_" or that_key, jbe/bsw@0: ref = ref, jbe/bsw@0: back_ref = back_ref, jbe/bsw@0: selector_generator = selector_generator or function(list, options) jbe/bsw@0: -- TODO: support tuple keys jbe/bsw@0: local options = options or {} jbe/bsw@0: local model = get_model() jbe/bsw@0: -- TODO: too many records cause PostgreSQL command stack overflow jbe/bsw@0: local ids = { sep = ", " } jbe/bsw@0: for i, object in ipairs(list) do jbe/bsw@0: local id = object[this_key] jbe/bsw@0: if id ~= nil then jbe/bsw@0: ids[#ids+1] = {"?", id} jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: if #ids == 0 then jbe/bsw@0: return model:new_selector():empty_list_mode() jbe/bsw@0: end jbe/bsw@0: local selector = model:new_selector() jbe/bsw@0: if connected_by_table then jbe/bsw@0: selector:join( jbe/bsw@0: connected_by_table, jbe/bsw@0: nil, jbe/bsw@0: { jbe/bsw@0: '$."$" = $."$"', jbe/bsw@0: {connected_by_table}, jbe/bsw@0: {connected_by_that_key}, jbe/bsw@0: {model:get_qualified_table()}, jbe/bsw@0: {that_key} jbe/bsw@0: } jbe/bsw@0: ) jbe/bsw@0: selector:add_field( jbe/bsw@0: { jbe/bsw@0: '$."$"', jbe/bsw@0: {connected_by_table}, jbe/bsw@0: {connected_by_this_key} jbe/bsw@0: }, jbe/bsw@0: 'mm_ref_' jbe/bsw@0: ) jbe/bsw@0: selector:add_where{ jbe/bsw@0: '$."$" IN ($)', jbe/bsw@0: {connected_by_table}, jbe/bsw@0: {connected_by_this_key}, jbe/bsw@0: ids jbe/bsw@0: } jbe/bsw@0: else jbe/bsw@0: selector:add_where{'"$" IN ($)', {that_key}, ids} jbe/bsw@0: end jbe/bsw@0: if options.order == nil and default_order then jbe/bsw@0: selector:add_order_by(default_order) jbe/bsw@0: elseif options.order then jbe/bsw@0: selector:add_order_by(options.order) jbe/bsw@0: end jbe/bsw@0: return selector jbe/bsw@0: end jbe/bsw@0: } jbe/bsw@0: if mode == "m1" or mode == "11" then jbe/bsw@0: self.foreign_keys[this_key] = ref jbe/bsw@0: end jbe/bsw@0: return self jbe/bsw@0: end