webmcp
view libraries/mondelefant/mondelefant.lua @ 0:9fdfb27f8e67
Version 1.0.0
| author | jbe/bsw | 
|---|---|
| date | Sun Oct 25 12:00:00 2009 +0100 (2009-10-25) | 
| parents | |
| children | 5e32ef998acf | 
 line source
     1 #!/usr/bin/env lua
     4 ---------------------------
     5 -- module initialization --
     6 ---------------------------
     8 local _G              = _G
     9 local _VERSION        = _VERSION
    10 local assert          = assert
    11 local collectgarbage  = collectgarbage
    12 local dofile          = dofile
    13 local error           = error
    14 local getfenv         = getfenv
    15 local getmetatable    = getmetatable
    16 local ipairs          = ipairs
    17 local load            = load
    18 local loadfile        = loadfile
    19 local loadstring      = loadstring
    20 local next            = next
    21 local pairs           = pairs
    22 local pcall           = pcall
    23 local print           = print
    24 local rawequal        = rawequal
    25 local rawget          = rawget
    26 local rawset          = rawset
    27 local select          = select
    28 local setfenv         = setfenv
    29 local setmetatable    = setmetatable
    30 local tonumber        = tonumber
    31 local tostring        = tostring
    32 local type            = type
    33 local unpack          = unpack
    34 local xpcall          = xpcall
    36 local coroutine       = coroutine
    37 local io              = io
    38 local math            = math
    39 local os              = os
    40 local string          = string
    41 local table           = table
    43 local add             = table.insert
    45 _G[...] = require("mondelefant_native")
    46 module(...)
    50 ---------------
    51 -- selectors --
    52 ---------------
    54 selector_metatable = {}
    55 selector_prototype = {}
    56 selector_metatable.__index = selector_prototype
    58 local function init_selector(self, db_conn)
    59   self._db_conn = db_conn
    60   self._mode = "list"
    61   self._fields = { sep = ", " }
    62   self._distinct = false
    63   self._distinct_on = {sep = ", ", expression}
    64   self._from = { sep = " " }
    65   self._where = { sep = " AND " }
    66   self._group_by = { sep = ", " }
    67   self._having = { sep = " AND " }
    68   self._combine = { sep = " " }
    69   self._order_by = { sep = ", " }
    70   self._limit = nil
    71   self._offset = nil
    72   --[[
    73   self._lock = nil
    74   self._lock_tables = { sep = ", " }
    75   --]]
    76   self._class = nil
    77   self._attach = nil
    78   return self
    79 end
    81 function connection_prototype:new_selector()
    82   return init_selector(setmetatable({}, selector_metatable), self)
    83 end
    85 function selector_prototype:get_db_conn()
    86   return self._db_conn
    87 end
    89 -- TODO: selector clone?
    91 function selector_prototype:single_object_mode()
    92   self._mode = "object"
    93   return self
    94 end
    96 function selector_prototype:optional_object_mode()
    97   self._mode = "opt_object"
    98   return self
    99 end
   101 function selector_prototype:empty_list_mode()
   102   self._mode = "empty_list"
   103   return self
   104 end
   106 function selector_prototype:add_distinct_on(expression)
   107   if self._distinct then
   108     error("Can not combine DISTINCT with DISTINCT ON.")
   109   end
   110   add(self._distinct_on, expression)
   111   return self
   112 end
   114 function selector_prototype:set_distinct()
   115   if #self._distinct_on > 0 then
   116     error("Can not combine DISTINCT with DISTINCT ON.")
   117   end
   118   self._distinct = true
   119   return self
   120 end
   122 function selector_prototype:add_from(expression, alias, condition)
   123   local first = (#self._from == 0)
   124   if not first then
   125     if condition then
   126       add(self._from, "INNER JOIN")
   127     else
   128       add(self._from, "CROSS JOIN")
   129     end
   130   end
   131   if getmetatable(expression) == selector_metatable then
   132     if alias then
   133       add(self._from, {'($) AS "$"', {expression}, {alias}})
   134     else
   135       add(self._from, {'($) AS "subquery"', {expression}})
   136     end
   137   else
   138     if alias then
   139       add(self._from, {'$ AS "$"', {expression}, {alias}})
   140     else
   141       add(self._from, expression)
   142     end
   143   end
   144   if condition then
   145     if first then
   146       self:condition(condition)
   147     else
   148       add(self._from, "ON")
   149       add(self._from, condition)
   150     end
   151   end
   152   return self
   153 end
   155 function selector_prototype:add_where(expression)
   156   add(self._where, expression)
   157   return self
   158 end
   160 function selector_prototype:add_group_by(expression)
   161   add(self._group_by, expression)
   162   return self
   163 end
   165 function selector_prototype:add_having(expression)
   166   add(self._having, expression)
   167   return self
   168 end
   170 function selector_prototype:add_combine(expression)
   171   add(self._combine, expression)
   172   return self
   173 end
   175 function selector_prototype:add_order_by(expression)
   176   add(self._order_by, expression)
   177   return self
   178 end
   180 function selector_prototype:limit(count)
   181   if type(count) ~= "number" or count % 1 ~= 0 then
   182     error("LIMIT must be an integer.")
   183   end
   184   self._limit = count
   185   return self
   186 end
   188 function selector_prototype:offset(count)
   189   if type(count) ~= "number" or count % 1 ~= 0 then
   190     error("OFFSET must be an integer.")
   191   end
   192   self._offset = count
   193   return self
   194 end
   196 function selector_prototype:reset_fields()
   197   for idx in ipairs(self._fields) do
   198     self._fields[idx] = nil
   199   end
   200   return self
   201 end
   203 function selector_prototype:add_field(expression, alias, options)
   204   if alias then
   205     add(self._fields, {'$ AS "$"', {expression}, {alias}})
   206   else
   207     add(self._fields, expression)
   208   end
   209   if options then
   210     for i, option in ipairs(options) do
   211       if option == "distinct" then
   212         if alias then
   213           self:add_distinct_on('"' .. alias .. '"')
   214         else
   215           self:add_distinct_on(expression)
   216         end
   217       elseif option == "grouped" then
   218         if alias then
   219           self:add_group_by('"' .. alias .. '"')
   220         else
   221           self:add_group_by(expression)
   222         end
   223       else
   224         error("Unknown option '" .. option .. "' to add_field method.")
   225       end
   226     end
   227   end
   228   return self
   229 end
   231 function selector_prototype:join(...)  -- NOTE: alias for add_from
   232   return self:add_from(...)
   233 end
   235 function selector_prototype:from(expression, alias, condition)
   236   if #self._from > 0 then
   237     error("From-clause already existing (hint: try join).")
   238   end
   239   return self:join(expression, alias, condition)
   240 end
   242 function selector_prototype:left_join(expression, alias, condition)
   243   local first = (#self._from == 0)
   244   if not first then
   245     add(self._from, "LEFT OUTER JOIN")
   246   end
   247   if alias then
   248     add(self._from, {'$ AS "$"', {expression}, {alias}})
   249   else
   250     add(self._from, expression)
   251   end
   252   if condition then
   253     if first then
   254       self:condition(condition)
   255     else
   256       add(self._from, "ON")
   257       add(self._from, condition)
   258     end
   259   end
   260   return self
   261 end
   263 function selector_prototype:union(expression)
   264   self:add_combine{"UNION $", {expression}}
   265   return self
   266 end
   268 function selector_prototype:union_all(expression)
   269   self:add_combine{"UNION ALL $", {expression}}
   270   return self
   271 end
   273 function selector_prototype:intersect(expression)
   274   self:add_combine{"INTERSECT $", {expression}}
   275   return self
   276 end
   278 function selector_prototype:intersect_all(expression)
   279   self:add_combine{"INTERSECT ALL $", {expression}}
   280   return self
   281 end
   283 function selector_prototype:except(expression)
   284   self:add_combine{"EXCEPT $", {expression}}
   285   return self
   286 end
   288 function selector_prototype:except_all(expression)
   289   self:add_combine{"EXCEPT ALL $", {expression}}
   290   return self
   291 end
   293 function selector_prototype:set_class(class)
   294   self._class = class
   295   return self
   296 end
   298 function selector_prototype:attach(mode, data2, field1, field2, ref1, ref2)
   299   self._attach = {
   300     mode = mode,
   301     data2 = data2,
   302     field1 = field1,
   303     field2 = field2,
   304     ref1 = ref1,
   305     ref2 = ref2
   306   }
   307   return self
   308 end
   310 -- TODO: many-to-many relations
   312 function selector_metatable:__tostring()
   313   local parts = {sep = " "}
   314   add(parts, "SELECT")
   315   if self._distinct then
   316     add(parts, "DISTINCT")
   317   elseif #self._distinct_on > 0 then
   318     add(parts, {"DISTINCT ON ($)", self._distinct_on})
   319   end
   320   add(parts, {"$", self._fields})
   321   if #self._from > 0 then
   322     add(parts, {"FROM $", self._from})
   323   end
   324   if #self._mode == "empty_list" then
   325     add(parts, "WHERE FALSE")
   326   elseif #self._where > 0 then
   327     add(parts, {"WHERE $", self._where})
   328   end
   329   if #self._group_by > 0 then
   330     add(parts, {"GROUP BY $", self._group_by})
   331   end
   332   if #self._having > 0 then
   333     add(parts, {"HAVING $", self._having})
   334   end
   335   for i, v in ipairs(self._combine) do
   336     add(parts, v)
   337   end
   338   if #self._order_by > 0 then
   339     add(parts, {"ORDER BY $", self._order_by})
   340   end
   341   if self._mode == "empty_list" then
   342     add(parts, "LIMIT 0")
   343   elseif self._mode ~= "list" then
   344     add(parts, "LIMIT 1")
   345   elseif self._limit then
   346     add(parts, "LIMIT " .. self._limit)
   347   end
   348   if self._offset then
   349     add(parts, "OFFSET " .. self._offset)
   350   end
   351   return self._db_conn:assemble_command{"$", parts}
   352 end
   354 function selector_prototype:try_exec()
   355   if self._mode == "empty_list" then
   356     if self._class then
   357       return nil, self._class:create_list()
   358     else
   359        return nil, self._db_conn:create_list()
   360     end
   361   end
   362   local db_error, db_result = self._db_conn:try_query(self, self._mode)
   363   if db_error then
   364     return db_error
   365   elseif db_result then
   366     if self._class then set_class(db_result, self._class) end
   367     if self._attach then
   368       attach(
   369         self._attach.mode,
   370         db_result,
   371         self._attach.data2,
   372         self._attach.field1,
   373         self._attach.field2,
   374         self._attach.ref1,
   375         self._attach.ref2
   376       )
   377     end
   378     return nil, db_result
   379   else
   380     return nil
   381   end
   382 end
   384 function selector_prototype:exec()
   385   local db_error, result = self:try_exec()
   386   if db_error then
   387     db_error:escalate()
   388   else
   389     return result
   390   end
   391 end
   395 -----------------
   396 -- attachments --
   397 -----------------
   399 local function attach_key(row, fields)
   400   local t = type(fields)
   401   if t == "string" then
   402     return tostring(row[fields])
   403   elseif t == "table" then
   404     local r = {}
   405     for idx, field in ipairs(fields) do
   406       r[idx] = string.format("%q", row[field])
   407     end
   408     return table.concat(r)
   409   else
   410     error("Field information for 'mondelefant.attach' is neither a string nor a table.")
   411   end
   412 end
   414 function attach(mode, data1, data2, key1, key2, ref1, ref2)
   415   local many1, many2
   416   if mode == "11" then
   417     many1 = false
   418     many2 = false
   419   elseif mode == "1m" then
   420     many1 = false
   421     many2 = true
   422   elseif mode == "m1" then
   423     many1 = true
   424     many2 = false
   425   elseif mode == "mm" then
   426     many1 = true
   427     many2 = true
   428   else
   429     error("Unknown mode specified for 'mondelefant.attach'.")
   430   end
   431   local list1, list2
   432   if data1._type == "object" then
   433     list1 = { data1 }
   434   elseif data1._type == "list" then
   435     list1 = data1
   436   else
   437     error("First result data given to 'mondelefant.attach' is invalid.")
   438   end
   439   if data2._type == "object" then
   440     list2 = { data2 }
   441   elseif data2._type == "list" then
   442     list2 = data2
   443   else
   444     error("Second result data given to 'mondelefant.attach' is invalid.")
   445   end
   446   local hash1 = {}
   447   local hash2 = {}
   448   if ref2 then
   449     for i, row in ipairs(list1) do
   450       local key = attach_key(row, key1)
   451       local list = hash1[key]
   452       if not list then list = {}; hash1[key] = list end
   453       list[#list + 1] = row
   454     end
   455   end
   456   if ref1 then
   457     for i, row in ipairs(list2) do
   458       local key = attach_key(row, key2)
   459       local list = hash2[key]
   460       if not list then list = {}; hash2[key] = list end
   461       list[#list + 1] = row
   462     end
   463     for i, row in ipairs(list1) do
   464       local key = attach_key(row, key1)
   465       local matching_rows = hash2[key]
   466       if many2 then
   467         local list = data2._connection:create_list(matching_rows)
   468         list._class = data2._class
   469         row._ref[ref1] = list
   470       elseif matching_rows and #matching_rows == 1 then
   471         row._ref[ref1] = matching_rows[1]
   472       else
   473         row._ref[ref1] = false
   474       end
   475     end
   476   end
   477   if ref2 then
   478     for i, row in ipairs(list2) do
   479       local key = attach_key(row, key2)
   480       local matching_rows = hash1[key]
   481       if many1 then
   482         local list = data1._connection:create_list(matching_rows)
   483         list._class = data1._class
   484         row._ref[ref2] = list
   485       elseif matching_rows and #matching_rows == 1 then
   486         row._ref[ref2] = matching_rows[1]
   487       else
   488         row._ref[ref2] = false
   489       end
   490     end
   491   end
   492 end
   496 ------------------
   497 -- model system --
   498 ------------------
   500 class_prototype.primary_key = "id"
   502 function class_prototype:get_db_conn()
   503   error(
   504     "Method mondelefant class(_prototype):get_db_conn() " ..
   505     "has to be implemented."
   506   )
   507 end
   509 function class_prototype:get_qualified_table()
   510   if not self.table then error "Table unknown." end
   511   if self.schema then
   512     return '"' .. self.schema .. '"."' .. self.table .. '"'
   513   else
   514     return '"' .. self.table .. '"'
   515   end
   516 end
   518 function class_prototype:get_qualified_table_literal()
   519   if not self.table then error "Table unknown." end
   520   if self.schema then
   521     return self.schema .. '.' .. self.table
   522   else
   523     return self.table
   524   end
   525 end
   527 function class_prototype:get_primary_key_list()
   528   local primary_key = self.primary_key
   529   if type(primary_key) == "string" then
   530     return {primary_key}
   531   else
   532     return primary_key
   533   end
   534 end
   536 function class_prototype:get_columns()
   537   if self._columns then
   538     return self._columns
   539   end
   540   local selector = self:get_db_conn():new_selector()
   541   selector:set_class(self)
   542   selector:from(self:get_qualified_table())
   543   selector:add_field("*")
   544   selector:add_where("FALSE")
   545   local db_result = selector:exec()
   546   local connection = db_result._connection
   547   local columns = {}
   548   for idx, info in ipairs(db_result._column_info) do
   549     local key   = info.field_name
   550     local value = {
   551       name = key,
   552       type = connection.type_mappings[info.type]
   553     }
   554     columns[key] = value
   555     table.insert(columns, value)
   556   end
   557   self._columns = columns
   558   return columns
   559 end
   561 function class_prototype:new_selector(db_conn)
   562   local selector = (db_conn or self:get_db_conn()):new_selector()
   563   selector:set_class(self)
   564   selector:from(self:get_qualified_table())
   565   selector:add_field(self:get_qualified_table() .. ".*")
   566   return selector
   567 end
   569 function class_prototype:create_list()
   570   local list = self:get_db_conn():create_list()
   571   list._class = self
   572   return list
   573 end
   575 function class_prototype:new()
   576   local object = self:get_db_conn():create_object()
   577   object._class = self
   578   object._new = true
   579   return object
   580 end
   582 function class_prototype.object:try_save()
   583   if not self._class then
   584     error("Cannot save object: No class information available.")
   585   end
   586   local primary_key = self._class:get_primary_key_list()
   587   local primary_key_sql = { sep = ", " }
   588   for idx, value in ipairs(primary_key) do
   589     primary_key_sql[idx] = '"' .. value .. '"'
   590   end
   591   if self._new then
   592     local fields = {sep = ", "}
   593     local values = {sep = ", "}
   594     for key, dummy in pairs(self._dirty or {}) do
   595       add(fields, {'"$"', {key}})
   596       add(values, {'?', self[key]})
   597     end
   598     if compat_returning then  -- compatibility for PostgreSQL 8.1
   599       local db_error, db_result1, db_result2 = self._connection:try_query(
   600         {
   601           'INSERT INTO $ ($) VALUES ($)',
   602           {self._class:get_qualified_table()},
   603           fields,
   604           values,
   605           primary_key_sql
   606         },
   607         "list",
   608         {
   609           'SELECT currval(?)',
   610           self._class.table .. '_id_seq'
   611         },
   612         "object"
   613       )
   614       if db_error then
   615         return db_error
   616       end
   617       self.id = db_result2.id
   618     else
   619       local db_error, db_result = self._connection:try_query(
   620         {
   621           'INSERT INTO $ ($) VALUES ($) RETURNING ($)',
   622           {self._class:get_qualified_table()},
   623           fields,
   624           values,
   625           primary_key_sql
   626         },
   627         "object"
   628       )
   629       if db_error then
   630         return db_error
   631       end
   632       for idx, value in ipairs(primary_key) do
   633         self[value] = db_result[value]
   634       end
   635     end
   636     self._new = false
   637   else
   638     local command_sets = {sep = ", "}
   639     for key, dummy in pairs(self._dirty or {}) do
   640       add(command_sets, {'"$" = ?', {key}, self[key]})
   641     end
   642     if #command_sets >= 1 then
   643       local primary_key_compare = {sep = " AND "}
   644       for idx, value in ipairs(primary_key) do
   645         primary_key_compare[idx] = {
   646           "$ = ?",
   647           {'"' .. value .. '"'},
   648           self[value]
   649         }
   650       end
   651       local db_error = self._connection:try_query{
   652         'UPDATE $ SET $ WHERE $',
   653         {self._class:get_qualified_table()},
   654         command_sets,
   655         primary_key_compare
   656       }
   657       if db_error then
   658         return db_error
   659       end
   660     end
   661   end
   662   return nil
   663 end
   665 function class_prototype.object:save()
   666   local db_error = self:try_save()
   667   if db_error then
   668     db_error:escalate()
   669   end
   670   return self
   671 end
   673 function class_prototype.object:try_destroy()
   674   if not self._class then
   675     error("Cannot destroy object: No class information available.")
   676   end
   677   local primary_key = self._class:get_primary_key_list()
   678   local primary_key_compare = {sep = " AND "}
   679   for idx, value in ipairs(primary_key) do
   680     primary_key_compare[idx] = {
   681       "$ = ?",
   682       {'"' .. value .. '"'},
   683       self[value]
   684     }
   685   end
   686   return self._connection:try_query{
   687     'DELETE FROM $ WHERE $',
   688     {self._class:get_qualified_table()},
   689     primary_key_compare
   690   }
   691 end
   693 function class_prototype.object:destroy()
   694   local db_error = self:try_destroy()
   695   if db_error then
   696     db_error:escalate()
   697   end
   698   return self
   699 end
   701 function class_prototype.list:get_reference_selector(
   702   ref_name, options, ref_alias, back_ref_alias
   703 )
   704   local ref_info = self._class.references[ref_name]
   705   if not ref_info then
   706     error('Reference with name "' .. ref_name .. '" not found.')
   707   end
   708   local selector = ref_info.selector_generator(self, options or {})
   709   local mode = ref_info.mode
   710   if mode == "mm" or mode == "1m" then
   711     mode = "m1"
   712   elseif mode == "m1" then
   713     mode = "1m"
   714   end
   715   local ref_alias = ref_alias
   716   if ref_alias == false then
   717     ref_alias = nil
   718   elseif ref_alias == nil then
   719     ref_alias = ref_name
   720   end
   721   local back_ref_alias
   722   if back_ref_alias == false then
   723     back_ref_alias = nil
   724   elseif back_ref_alias == nil then
   725     back_ref_alias = ref_info.back_ref
   726   end
   727   selector:attach(
   728     mode,
   729     self,
   730     ref_info.that_key,                   ref_info.this_key,
   731     back_ref_alias or ref_info.back_ref, ref_alias or ref_name
   732   )
   733   return selector
   734 end
   736 function class_prototype.list.load(...)
   737   return class_prototype.list.get_reference_selector(...):exec()
   738 end
   740 function class_prototype.object:get_reference_selector(...)
   741   local list = self._class:create_list()
   742   list[1] = self
   743   return list:get_reference_selector(...)
   744 end
   746 function class_prototype.object.load(...)
   747   return class_prototype.object.get_reference_selector(...):exec()
   748 end
   751 function class_prototype:add_reference(args)
   752   local selector_generator    = args.selector_generator
   753   local mode                  = args.mode
   754   local to                    = args.to
   755   local this_key              = args.this_key
   756   local that_key              = args.that_key
   757   local connected_by_table    = args.connected_by_table  -- TODO: split to table and schema
   758   local connected_by_this_key = args.connected_by_this_key
   759   local connected_by_that_key = args.connected_by_that_key
   760   local ref                   = args.ref
   761   local back_ref              = args.back_ref
   762   local default_order         = args.default_order
   763   local model
   764   local function get_model()
   765     if not model then
   766       if type(to) == "string" then
   767         model = _G
   768         for path_element in string.gmatch(to, "[^.]+") do
   769           model = model[path_element]
   770         end
   771       elseif type(to) == "function" then
   772         model = to()
   773       else
   774         model = to
   775       end
   776     end
   777     if not model or model == _G then
   778       error("Could not get model for reference.")
   779     end
   780     return model
   781   end
   782   self.references[ref] = {
   783     mode     = mode,
   784     this_key = this_key,
   785     that_key = connected_by_table and "mm_ref_" or that_key,
   786     ref      = ref,
   787     back_ref = back_ref,
   788     selector_generator = selector_generator or function(list, options)
   789       -- TODO: support tuple keys
   790       local options = options or {}
   791       local model = get_model()
   792       -- TODO: too many records cause PostgreSQL command stack overflow
   793       local ids = { sep = ", " }
   794       for i, object in ipairs(list) do
   795         local id = object[this_key]
   796         if id ~= nil then
   797           ids[#ids+1] = {"?", id}
   798         end
   799       end
   800       if #ids == 0 then
   801         return model:new_selector():empty_list_mode()
   802       end
   803       local selector = model:new_selector()
   804       if connected_by_table then
   805         selector:join(
   806           connected_by_table,
   807           nil,
   808           {
   809             '$."$" = $."$"',
   810             {connected_by_table},
   811             {connected_by_that_key},
   812             {model:get_qualified_table()},
   813             {that_key}
   814           }
   815         )
   816         selector:add_field(
   817           {
   818             '$."$"',
   819             {connected_by_table},
   820             {connected_by_this_key}
   821           },
   822           'mm_ref_'
   823         )
   824         selector:add_where{
   825           '$."$" IN ($)',
   826           {connected_by_table},
   827           {connected_by_this_key},
   828           ids
   829         }
   830       else
   831         selector:add_where{'"$" IN ($)', {that_key}, ids}
   832       end
   833       if options.order == nil and default_order then
   834         selector:add_order_by(default_order)
   835       elseif options.order then
   836         selector:add_order_by(options.order)
   837       end
   838       return selector
   839     end
   840   }
   841   if mode == "m1" or mode == "11" then
   842     self.foreign_keys[this_key] = ref
   843   end
   844   return self
   845 end
