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)
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

Impressum / About Us