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

Impressum / About Us