webmcp
view libraries/mondelefant/mondelefant.lua @ 4:5e32ef998acf
Version 1.0.4
ui.link{...} with POST target can now be parameterized with BOTH content and text to allow HTML content for JavaScript browsers and a text-only version for accessiblity
Changes related to database selectors:
- Support for row-based locking
- New method :count(), caching and returning the number of rows, which WOULD have been returned by :exec()
- Bugfix: WHERE and HAVING expressions are now enclosed in parenthesis to avoid problems with operator precedence
ui.script{...} now supports external .js files
Changes in langtool.lua to cope with escaped new-line chars (\n)
ui.link{...} with POST target can now be parameterized with BOTH content and text to allow HTML content for JavaScript browsers and a text-only version for accessiblity
Changes related to database selectors:
- Support for row-based locking
- New method :count(), caching and returning the number of rows, which WOULD have been returned by :exec()
- Bugfix: WHERE and HAVING expressions are now enclosed in parenthesis to avoid problems with operator precedence
ui.script{...} now supports external .js files
Changes in langtool.lua to cope with escaped new-line chars (\n)
| author | jbe/bsw | 
|---|---|
| date | Fri Dec 25 12:00:00 2009 +0100 (2009-12-25) | 
| parents | 9fdfb27f8e67 | 
| children | 5cba83b3f411 | 
 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   self._read_lock = { sep = ", " }
    73   self._write_lock = { sep = ", " }
    74   self._class = nil
    75   self._attach = nil
    76   return self
    77 end
    79 function connection_prototype:new_selector()
    80   return init_selector(setmetatable({}, selector_metatable), self)
    81 end
    83 function selector_prototype:get_db_conn()
    84   return self._db_conn
    85 end
    87 -- TODO: selector clone?
    89 function selector_prototype:single_object_mode()
    90   self._mode = "object"
    91   return self
    92 end
    94 function selector_prototype:optional_object_mode()
    95   self._mode = "opt_object"
    96   return self
    97 end
    99 function selector_prototype:empty_list_mode()
   100   self._mode = "empty_list"
   101   return self
   102 end
   104 function selector_prototype:add_distinct_on(expression)
   105   if self._distinct then
   106     error("Can not combine DISTINCT with DISTINCT ON.")
   107   end
   108   add(self._distinct_on, expression)
   109   return self
   110 end
   112 function selector_prototype:set_distinct()
   113   if #self._distinct_on > 0 then
   114     error("Can not combine DISTINCT with DISTINCT ON.")
   115   end
   116   self._distinct = true
   117   return self
   118 end
   120 function selector_prototype:add_from(expression, alias, condition)
   121   local first = (#self._from == 0)
   122   if not first then
   123     if condition then
   124       add(self._from, "INNER JOIN")
   125     else
   126       add(self._from, "CROSS JOIN")
   127     end
   128   end
   129   if getmetatable(expression) == selector_metatable then
   130     if alias then
   131       add(self._from, {'($) AS "$"', {expression}, {alias}})
   132     else
   133       add(self._from, {'($) AS "subquery"', {expression}})
   134     end
   135   else
   136     if alias then
   137       add(self._from, {'$ AS "$"', {expression}, {alias}})
   138     else
   139       add(self._from, expression)
   140     end
   141   end
   142   if condition then
   143     if first then
   144       self:condition(condition)
   145     else
   146       add(self._from, "ON")
   147       add(self._from, condition)
   148     end
   149   end
   150   return self
   151 end
   153 function selector_prototype:add_where(expression)
   154   add(self._where, expression)
   155   return self
   156 end
   158 function selector_prototype:add_group_by(expression)
   159   add(self._group_by, expression)
   160   return self
   161 end
   163 function selector_prototype:add_having(expression)
   164   add(self._having, expression)
   165   return self
   166 end
   168 function selector_prototype:add_combine(expression)
   169   add(self._combine, expression)
   170   return self
   171 end
   173 function selector_prototype:add_order_by(expression)
   174   add(self._order_by, expression)
   175   return self
   176 end
   178 function selector_prototype:limit(count)
   179   if type(count) ~= "number" or count % 1 ~= 0 then
   180     error("LIMIT must be an integer.")
   181   end
   182   self._limit = count
   183   return self
   184 end
   186 function selector_prototype:offset(count)
   187   if type(count) ~= "number" or count % 1 ~= 0 then
   188     error("OFFSET must be an integer.")
   189   end
   190   self._offset = count
   191   return self
   192 end
   194 function selector_prototype:for_share()
   195   self._read_lock.all = true
   196   return self
   197 end
   199 function selector_prototype:for_share_of(expression)
   200   add(self._read_lock, expression)
   201   return self
   202 end
   204 function selector_prototype:for_update()
   205   self._write_lock.all = true
   206   return self
   207 end
   209 function selector_prototype:for_update_of(expression)
   210   add(self._write_lock, expression)
   211   return self
   212 end
   214 function selector_prototype:reset_fields()
   215   for idx in ipairs(self._fields) do
   216     self._fields[idx] = nil
   217   end
   218   return self
   219 end
   221 function selector_prototype:add_field(expression, alias, options)
   222   if alias then
   223     add(self._fields, {'$ AS "$"', {expression}, {alias}})
   224   else
   225     add(self._fields, expression)
   226   end
   227   if options then
   228     for i, option in ipairs(options) do
   229       if option == "distinct" then
   230         if alias then
   231           self:add_distinct_on('"' .. alias .. '"')
   232         else
   233           self:add_distinct_on(expression)
   234         end
   235       elseif option == "grouped" then
   236         if alias then
   237           self:add_group_by('"' .. alias .. '"')
   238         else
   239           self:add_group_by(expression)
   240         end
   241       else
   242         error("Unknown option '" .. option .. "' to add_field method.")
   243       end
   244     end
   245   end
   246   return self
   247 end
   249 function selector_prototype:join(...)  -- NOTE: alias for add_from
   250   return self:add_from(...)
   251 end
   253 function selector_prototype:from(expression, alias, condition)
   254   if #self._from > 0 then
   255     error("From-clause already existing (hint: try join).")
   256   end
   257   return self:join(expression, alias, condition)
   258 end
   260 function selector_prototype:left_join(expression, alias, condition)
   261   local first = (#self._from == 0)
   262   if not first then
   263     add(self._from, "LEFT OUTER JOIN")
   264   end
   265   if alias then
   266     add(self._from, {'$ AS "$"', {expression}, {alias}})
   267   else
   268     add(self._from, expression)
   269   end
   270   if condition then
   271     if first then
   272       self:condition(condition)
   273     else
   274       add(self._from, "ON")
   275       add(self._from, condition)
   276     end
   277   end
   278   return self
   279 end
   281 function selector_prototype:union(expression)
   282   self:add_combine{"UNION $", {expression}}
   283   return self
   284 end
   286 function selector_prototype:union_all(expression)
   287   self:add_combine{"UNION ALL $", {expression}}
   288   return self
   289 end
   291 function selector_prototype:intersect(expression)
   292   self:add_combine{"INTERSECT $", {expression}}
   293   return self
   294 end
   296 function selector_prototype:intersect_all(expression)
   297   self:add_combine{"INTERSECT ALL $", {expression}}
   298   return self
   299 end
   301 function selector_prototype:except(expression)
   302   self:add_combine{"EXCEPT $", {expression}}
   303   return self
   304 end
   306 function selector_prototype:except_all(expression)
   307   self:add_combine{"EXCEPT ALL $", {expression}}
   308   return self
   309 end
   311 function selector_prototype:set_class(class)
   312   self._class = class
   313   return self
   314 end
   316 function selector_prototype:attach(mode, data2, field1, field2, ref1, ref2)
   317   self._attach = {
   318     mode = mode,
   319     data2 = data2,
   320     field1 = field1,
   321     field2 = field2,
   322     ref1 = ref1,
   323     ref2 = ref2
   324   }
   325   return self
   326 end
   328 -- TODO: many-to-many relations
   330 function selector_metatable:__tostring()
   331   local parts = {sep = " "}
   332   add(parts, "SELECT")
   333   if self._distinct then
   334     add(parts, "DISTINCT")
   335   elseif #self._distinct_on > 0 then
   336     add(parts, {"DISTINCT ON ($)", self._distinct_on})
   337   end
   338   add(parts, {"$", self._fields})
   339   if #self._from > 0 then
   340     add(parts, {"FROM $", self._from})
   341   end
   342   if #self._mode == "empty_list" then
   343     add(parts, "WHERE FALSE")
   344   elseif #self._where > 0 then
   345     add(parts, {"WHERE ($)", self._where})
   346   end
   347   if #self._group_by > 0 then
   348     add(parts, {"GROUP BY $", self._group_by})
   349   end
   350   if #self._having > 0 then
   351     add(parts, {"HAVING ($)", self._having})
   352   end
   353   for i, v in ipairs(self._combine) do
   354     add(parts, v)
   355   end
   356   if #self._order_by > 0 then
   357     add(parts, {"ORDER BY $", self._order_by})
   358   end
   359   if self._mode == "empty_list" then
   360     add(parts, "LIMIT 0")
   361   elseif self._mode ~= "list" then
   362     add(parts, "LIMIT 1")
   363   elseif self._limit then
   364     add(parts, "LIMIT " .. self._limit)
   365   end
   366   if self._offset then
   367     add(parts, "OFFSET " .. self._offset)
   368   end
   369   if self._write_lock.all then
   370     add(parts, "FOR UPDATE")
   371   else
   372     if self._read_lock.all then
   373       add(parts, "FOR SHARE")
   374     elseif #self._read_lock > 0 then
   375       add(parts, {"FOR SHARE OF $", self._read_lock})
   376     end
   377     if #self._write_lock > 0 then
   378       add(parts, {"FOR UPDATE OF $", self._write_lock})
   379     end
   380   end
   381   return self._db_conn:assemble_command{"$", parts}
   382 end
   384 function selector_prototype:try_exec()
   385   if self._mode == "empty_list" then
   386     if self._class then
   387       return nil, self._class:create_list()
   388     else
   389        return nil, self._db_conn:create_list()
   390     end
   391   end
   392   local db_error, db_result = self._db_conn:try_query(self, self._mode)
   393   if db_error then
   394     return db_error
   395   elseif db_result then
   396     if self._class then set_class(db_result, self._class) end
   397     if self._attach then
   398       attach(
   399         self._attach.mode,
   400         db_result,
   401         self._attach.data2,
   402         self._attach.field1,
   403         self._attach.field2,
   404         self._attach.ref1,
   405         self._attach.ref2
   406       )
   407     end
   408     return nil, db_result
   409   else
   410     return nil
   411   end
   412 end
   414 function selector_prototype:exec()
   415   local db_error, result = self:try_exec()
   416   if db_error then
   417     db_error:escalate()
   418   else
   419     return result
   420   end
   421 end
   423 -- NOTE: This function caches the result!
   424 function selector_prototype:count()
   425   if not self._count then
   426     local count_selector = self:get_db_conn():new_selector()
   427     count_selector:add_field('count(1)')
   428     count_selector:add_from(self)
   429     count_selector:single_object_mode()
   430     self._count = count_selector:exec().count
   431   end
   432   return self._count
   433 end
   437 -----------------
   438 -- attachments --
   439 -----------------
   441 local function attach_key(row, fields)
   442   local t = type(fields)
   443   if t == "string" then
   444     return tostring(row[fields])
   445   elseif t == "table" then
   446     local r = {}
   447     for idx, field in ipairs(fields) do
   448       r[idx] = string.format("%q", row[field])
   449     end
   450     return table.concat(r)
   451   else
   452     error("Field information for 'mondelefant.attach' is neither a string nor a table.")
   453   end
   454 end
   456 function attach(mode, data1, data2, key1, key2, ref1, ref2)
   457   local many1, many2
   458   if mode == "11" then
   459     many1 = false
   460     many2 = false
   461   elseif mode == "1m" then
   462     many1 = false
   463     many2 = true
   464   elseif mode == "m1" then
   465     many1 = true
   466     many2 = false
   467   elseif mode == "mm" then
   468     many1 = true
   469     many2 = true
   470   else
   471     error("Unknown mode specified for 'mondelefant.attach'.")
   472   end
   473   local list1, list2
   474   if data1._type == "object" then
   475     list1 = { data1 }
   476   elseif data1._type == "list" then
   477     list1 = data1
   478   else
   479     error("First result data given to 'mondelefant.attach' is invalid.")
   480   end
   481   if data2._type == "object" then
   482     list2 = { data2 }
   483   elseif data2._type == "list" then
   484     list2 = data2
   485   else
   486     error("Second result data given to 'mondelefant.attach' is invalid.")
   487   end
   488   local hash1 = {}
   489   local hash2 = {}
   490   if ref2 then
   491     for i, row in ipairs(list1) do
   492       local key = attach_key(row, key1)
   493       local list = hash1[key]
   494       if not list then list = {}; hash1[key] = list end
   495       list[#list + 1] = row
   496     end
   497   end
   498   if ref1 then
   499     for i, row in ipairs(list2) do
   500       local key = attach_key(row, key2)
   501       local list = hash2[key]
   502       if not list then list = {}; hash2[key] = list end
   503       list[#list + 1] = row
   504     end
   505     for i, row in ipairs(list1) do
   506       local key = attach_key(row, key1)
   507       local matching_rows = hash2[key]
   508       if many2 then
   509         local list = data2._connection:create_list(matching_rows)
   510         list._class = data2._class
   511         row._ref[ref1] = list
   512       elseif matching_rows and #matching_rows == 1 then
   513         row._ref[ref1] = matching_rows[1]
   514       else
   515         row._ref[ref1] = false
   516       end
   517     end
   518   end
   519   if ref2 then
   520     for i, row in ipairs(list2) do
   521       local key = attach_key(row, key2)
   522       local matching_rows = hash1[key]
   523       if many1 then
   524         local list = data1._connection:create_list(matching_rows)
   525         list._class = data1._class
   526         row._ref[ref2] = list
   527       elseif matching_rows and #matching_rows == 1 then
   528         row._ref[ref2] = matching_rows[1]
   529       else
   530         row._ref[ref2] = false
   531       end
   532     end
   533   end
   534 end
   538 ------------------
   539 -- model system --
   540 ------------------
   542 class_prototype.primary_key = "id"
   544 function class_prototype:get_db_conn()
   545   error(
   546     "Method mondelefant class(_prototype):get_db_conn() " ..
   547     "has to be implemented."
   548   )
   549 end
   551 function class_prototype:get_qualified_table()
   552   if not self.table then error "Table unknown." end
   553   if self.schema then
   554     return '"' .. self.schema .. '"."' .. self.table .. '"'
   555   else
   556     return '"' .. self.table .. '"'
   557   end
   558 end
   560 function class_prototype:get_qualified_table_literal()
   561   if not self.table then error "Table unknown." end
   562   if self.schema then
   563     return self.schema .. '.' .. self.table
   564   else
   565     return self.table
   566   end
   567 end
   569 function class_prototype:get_primary_key_list()
   570   local primary_key = self.primary_key
   571   if type(primary_key) == "string" then
   572     return {primary_key}
   573   else
   574     return primary_key
   575   end
   576 end
   578 function class_prototype:get_columns()
   579   if self._columns then
   580     return self._columns
   581   end
   582   local selector = self:get_db_conn():new_selector()
   583   selector:set_class(self)
   584   selector:from(self:get_qualified_table())
   585   selector:add_field("*")
   586   selector:add_where("FALSE")
   587   local db_result = selector:exec()
   588   local connection = db_result._connection
   589   local columns = {}
   590   for idx, info in ipairs(db_result._column_info) do
   591     local key   = info.field_name
   592     local value = {
   593       name = key,
   594       type = connection.type_mappings[info.type]
   595     }
   596     columns[key] = value
   597     table.insert(columns, value)
   598   end
   599   self._columns = columns
   600   return columns
   601 end
   603 function class_prototype:new_selector(db_conn)
   604   local selector = (db_conn or self:get_db_conn()):new_selector()
   605   selector:set_class(self)
   606   selector:from(self:get_qualified_table())
   607   selector:add_field(self:get_qualified_table() .. ".*")
   608   return selector
   609 end
   611 function class_prototype:create_list()
   612   local list = self:get_db_conn():create_list()
   613   list._class = self
   614   return list
   615 end
   617 function class_prototype:new()
   618   local object = self:get_db_conn():create_object()
   619   object._class = self
   620   object._new = true
   621   return object
   622 end
   624 function class_prototype.object:try_save()
   625   if not self._class then
   626     error("Cannot save object: No class information available.")
   627   end
   628   local primary_key = self._class:get_primary_key_list()
   629   local primary_key_sql = { sep = ", " }
   630   for idx, value in ipairs(primary_key) do
   631     primary_key_sql[idx] = '"' .. value .. '"'
   632   end
   633   if self._new then
   634     local fields = {sep = ", "}
   635     local values = {sep = ", "}
   636     for key, dummy in pairs(self._dirty or {}) do
   637       add(fields, {'"$"', {key}})
   638       add(values, {'?', self[key]})
   639     end
   640     if compat_returning then  -- compatibility for PostgreSQL 8.1
   641       local db_error, db_result1, db_result2 = self._connection:try_query(
   642         {
   643           'INSERT INTO $ ($) VALUES ($)',
   644           {self._class:get_qualified_table()},
   645           fields,
   646           values,
   647           primary_key_sql
   648         },
   649         "list",
   650         {
   651           'SELECT currval(?)',
   652           self._class.table .. '_id_seq'
   653         },
   654         "object"
   655       )
   656       if db_error then
   657         return db_error
   658       end
   659       self.id = db_result2.id
   660     else
   661       local db_error, db_result = self._connection:try_query(
   662         {
   663           'INSERT INTO $ ($) VALUES ($) RETURNING ($)',
   664           {self._class:get_qualified_table()},
   665           fields,
   666           values,
   667           primary_key_sql
   668         },
   669         "object"
   670       )
   671       if db_error then
   672         return db_error
   673       end
   674       for idx, value in ipairs(primary_key) do
   675         self[value] = db_result[value]
   676       end
   677     end
   678     self._new = false
   679   else
   680     local command_sets = {sep = ", "}
   681     for key, dummy in pairs(self._dirty or {}) do
   682       add(command_sets, {'"$" = ?', {key}, self[key]})
   683     end
   684     if #command_sets >= 1 then
   685       local primary_key_compare = {sep = " AND "}
   686       for idx, value in ipairs(primary_key) do
   687         primary_key_compare[idx] = {
   688           "$ = ?",
   689           {'"' .. value .. '"'},
   690           self[value]
   691         }
   692       end
   693       local db_error = self._connection:try_query{
   694         'UPDATE $ SET $ WHERE $',
   695         {self._class:get_qualified_table()},
   696         command_sets,
   697         primary_key_compare
   698       }
   699       if db_error then
   700         return db_error
   701       end
   702     end
   703   end
   704   return nil
   705 end
   707 function class_prototype.object:save()
   708   local db_error = self:try_save()
   709   if db_error then
   710     db_error:escalate()
   711   end
   712   return self
   713 end
   715 function class_prototype.object:try_destroy()
   716   if not self._class then
   717     error("Cannot destroy object: No class information available.")
   718   end
   719   local primary_key = self._class:get_primary_key_list()
   720   local primary_key_compare = {sep = " AND "}
   721   for idx, value in ipairs(primary_key) do
   722     primary_key_compare[idx] = {
   723       "$ = ?",
   724       {'"' .. value .. '"'},
   725       self[value]
   726     }
   727   end
   728   return self._connection:try_query{
   729     'DELETE FROM $ WHERE $',
   730     {self._class:get_qualified_table()},
   731     primary_key_compare
   732   }
   733 end
   735 function class_prototype.object:destroy()
   736   local db_error = self:try_destroy()
   737   if db_error then
   738     db_error:escalate()
   739   end
   740   return self
   741 end
   743 function class_prototype.list:get_reference_selector(
   744   ref_name, options, ref_alias, back_ref_alias
   745 )
   746   local ref_info = self._class.references[ref_name]
   747   if not ref_info then
   748     error('Reference with name "' .. ref_name .. '" not found.')
   749   end
   750   local selector = ref_info.selector_generator(self, options or {})
   751   local mode = ref_info.mode
   752   if mode == "mm" or mode == "1m" then
   753     mode = "m1"
   754   elseif mode == "m1" then
   755     mode = "1m"
   756   end
   757   local ref_alias = ref_alias
   758   if ref_alias == false then
   759     ref_alias = nil
   760   elseif ref_alias == nil then
   761     ref_alias = ref_name
   762   end
   763   local back_ref_alias
   764   if back_ref_alias == false then
   765     back_ref_alias = nil
   766   elseif back_ref_alias == nil then
   767     back_ref_alias = ref_info.back_ref
   768   end
   769   selector:attach(
   770     mode,
   771     self,
   772     ref_info.that_key,                   ref_info.this_key,
   773     back_ref_alias or ref_info.back_ref, ref_alias or ref_name
   774   )
   775   return selector
   776 end
   778 function class_prototype.list.load(...)
   779   return class_prototype.list.get_reference_selector(...):exec()
   780 end
   782 function class_prototype.object:get_reference_selector(...)
   783   local list = self._class:create_list()
   784   list[1] = self
   785   return list:get_reference_selector(...)
   786 end
   788 function class_prototype.object.load(...)
   789   return class_prototype.object.get_reference_selector(...):exec()
   790 end
   793 function class_prototype:add_reference(args)
   794   local selector_generator    = args.selector_generator
   795   local mode                  = args.mode
   796   local to                    = args.to
   797   local this_key              = args.this_key
   798   local that_key              = args.that_key
   799   local connected_by_table    = args.connected_by_table  -- TODO: split to table and schema
   800   local connected_by_this_key = args.connected_by_this_key
   801   local connected_by_that_key = args.connected_by_that_key
   802   local ref                   = args.ref
   803   local back_ref              = args.back_ref
   804   local default_order         = args.default_order
   805   local model
   806   local function get_model()
   807     if not model then
   808       if type(to) == "string" then
   809         model = _G
   810         for path_element in string.gmatch(to, "[^.]+") do
   811           model = model[path_element]
   812         end
   813       elseif type(to) == "function" then
   814         model = to()
   815       else
   816         model = to
   817       end
   818     end
   819     if not model or model == _G then
   820       error("Could not get model for reference.")
   821     end
   822     return model
   823   end
   824   self.references[ref] = {
   825     mode     = mode,
   826     this_key = this_key,
   827     that_key = connected_by_table and "mm_ref_" or that_key,
   828     ref      = ref,
   829     back_ref = back_ref,
   830     selector_generator = selector_generator or function(list, options)
   831       -- TODO: support tuple keys
   832       local options = options or {}
   833       local model = get_model()
   834       -- TODO: too many records cause PostgreSQL command stack overflow
   835       local ids = { sep = ", " }
   836       for i, object in ipairs(list) do
   837         local id = object[this_key]
   838         if id ~= nil then
   839           ids[#ids+1] = {"?", id}
   840         end
   841       end
   842       if #ids == 0 then
   843         return model:new_selector():empty_list_mode()
   844       end
   845       local selector = model:new_selector()
   846       if connected_by_table then
   847         selector:join(
   848           connected_by_table,
   849           nil,
   850           {
   851             '$."$" = $."$"',
   852             {connected_by_table},
   853             {connected_by_that_key},
   854             {model:get_qualified_table()},
   855             {that_key}
   856           }
   857         )
   858         selector:add_field(
   859           {
   860             '$."$"',
   861             {connected_by_table},
   862             {connected_by_this_key}
   863           },
   864           'mm_ref_'
   865         )
   866         selector:add_where{
   867           '$."$" IN ($)',
   868           {connected_by_table},
   869           {connected_by_this_key},
   870           ids
   871         }
   872       else
   873         selector:add_where{'"$" IN ($)', {that_key}, ids}
   874       end
   875       if options.order == nil and default_order then
   876         selector:add_order_by(default_order)
   877       elseif options.order then
   878         selector:add_order_by(options.order)
   879       end
   880       return selector
   881     end
   882   }
   883   if mode == "m1" or mode == "11" then
   884     self.foreign_keys[this_key] = ref
   885   end
   886   return self
   887 end
