File Coverage

blib/lib/auto/share/dist/App-Dochazka-REST/sql/dbinit_Config.pm
Criterion Covered Total %
statement 6 6 100.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 8 8 100.0


line stmt bran cond sub pod time code
1             # *************************************************************************
2             # Copyright (c) 2014-2017, SUSE LLC
3             #
4             # All rights reserved.
5             #
6             # Redistribution and use in source and binary forms, with or without
7             # modification, are permitted provided that the following conditions are met:
8             #
9             # 1. Redistributions of source code must retain the above copyright notice,
10             # this list of conditions and the following disclaimer.
11             #
12             # 2. Redistributions in binary form must reproduce the above copyright
13             # notice, this list of conditions and the following disclaimer in the
14             # documentation and/or other materials provided with the distribution.
15             #
16             # 3. Neither the name of SUSE LLC nor the names of its contributors may be
17             # used to endorse or promote products derived from this software without
18             # specific prior written permission.
19             #
20             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30             # POSSIBILITY OF SUCH DAMAGE.
31             # *************************************************************************
32             #
33             # sql/dbinit_Config.pm
34             #
35             # database initialization SQL
36              
37             #
38             # DBINIT_CONNECT_SUPERUSER
39             # DBINIT_CONNECT_SUPERAUTH
40             #
41             # These should be overrided in Dochazka_SiteConfig.pm with real
42             # superuser credentials (but only for testing - do not put production
43             # credentials in any configuration file!!!!)
44             #
45             set( 'DBINIT_CONNECT_SUPERUSER', 'postgres' );
46             set( 'DBINIT_CONNECT_SUPERAUTH', 'bogus_password_to_be_overrided' );
47              
48             #
49             # DBINIT_CREATE
50             #
51             # A list of SQL statements that are executed when the database is first
52             # created, to set up the table structure, etc. -- see the create_tables
53             # subroutine in REST.pm
54             #
55             set( 'DBINIT_CREATE', [
56              
57             # miscellaneous settings
58              
59             q/SET client_min_messages=WARNING/,
60              
61             # generalized (utility) functions used in multiple datamodel classes
62              
63             q#CREATE OR REPLACE FUNCTION round_time(timestamptz)
64             RETURNS TIMESTAMPTZ AS $$
65             SELECT date_trunc('hour', $1) + INTERVAL '5 min' * ROUND(date_part('minute', $1) / 5.0)
66             $$ LANGUAGE sql IMMUTABLE
67             #,
68              
69             q#COMMENT ON FUNCTION round_time(timestamptz) IS
70             'Round a single timestamp value to the nearest 5 minutes'#,
71              
72             q#CREATE OR REPLACE FUNCTION parens(tstzrange)
73             RETURNS RECORD AS $$
74             DECLARE
75             left_paren text;
76             right_paren text;
77             BEGIN
78             IF lower_inc($1) THEN
79             left_paren := '['::text;
80             ELSE
81             left_paren := '('::text;
82             END IF;
83             IF upper_inc($1) THEN
84             right_paren := ']'::text;
85             ELSE
86             right_paren := ')'::text;
87             END IF;
88             RETURN (left_paren, right_paren);
89             END;
90             $$ LANGUAGE plpgsql#,
91              
92             q/CREATE OR REPLACE FUNCTION overlaps(tstzrange, tstzrange)
93             RETURNS boolean AS $$
94             BEGIN
95             IF $1 && $2 THEN
96             RETURN 't'::boolean;
97             ELSE
98             RETURN 'f'::boolean;
99             END IF;
100             END;
101             $$ LANGUAGE plpgsql/,
102              
103             q/COMMENT ON FUNCTION overlaps(tstzrange, tstzrange) IS
104             'Tests two tstzranges whether they overlap'/,
105              
106             q/CREATE OR REPLACE FUNCTION not_before_1892(timestamptz)
107             RETURNS TIMESTAMPTZ AS $IMM$
108             BEGIN
109             IF $1 < '1892-01-01'::timestamptz THEN
110             RAISE EXCEPTION 'No dates earlier than 1892-01-01 please';
111             END IF;
112             RETURN $1;
113             END;
114             $IMM$ LANGUAGE plpgsql/,
115              
116             q/COMMENT ON FUNCTION not_before_1892(timestamptz) IS 'We enforce dates 1892-01-01 or later'/,
117              
118             q#CREATE OR REPLACE FUNCTION valid_intvl() RETURNS trigger AS $$
119             BEGIN
120             IF ( NEW.intvl IS NULL ) OR
121             ( isempty(NEW.intvl) ) OR
122             ( lower(NEW.intvl) = '-infinity' ) OR
123             ( lower(NEW.intvl) = 'infinity' ) OR
124             ( upper(NEW.intvl) = '-infinity' ) OR
125             ( upper(NEW.intvl) = 'infinity' ) OR
126             ( NOT lower_inc(NEW.intvl) ) OR
127             ( upper_inc(NEW.intvl) ) OR
128             ( lower_inf(NEW.intvl) ) OR
129             ( upper_inf(NEW.intvl) ) THEN
130             RAISE EXCEPTION 'illegal attendance interval %s', NEW.intvl;
131             END IF;
132             PERFORM not_before_1892(upper(NEW.intvl));
133             PERFORM not_before_1892(lower(NEW.intvl));
134             IF ( upper(NEW.intvl) != round_time(upper(NEW.intvl)) ) OR
135             ( lower(NEW.intvl) != round_time(lower(NEW.intvl)) ) THEN
136             RAISE EXCEPTION 'upper and lower bounds of interval must be evenly divisible by 5 minutes';
137             END IF;
138             RETURN NEW;
139             END;
140             $$ LANGUAGE plpgsql IMMUTABLE
141             #,
142              
143             q#COMMENT ON FUNCTION valid_intvl() IS $body$
144             This function runs a battery of validation tests on intervals.
145             The purpose of these tests is to ensure that the only intervals to make
146             it into the database are those that make sense in the context of employee
147             attendance.
148             $body$
149             #,
150              
151             # the 'employees' table
152              
153             q/CREATE TABLE IF NOT EXISTS employees (
154             eid serial PRIMARY KEY,
155             nick varchar(32) UNIQUE NOT NULL,
156             sec_id varchar(64) UNIQUE,
157             fullname varchar(96) UNIQUE,
158             email text UNIQUE,
159             passhash text,
160             salt text,
161             sync boolean DEFAULT FALSE NOT NULL,
162             supervisor integer REFERENCES employees (eid),
163             remark text,
164             CONSTRAINT kosher_nick CHECK (nick ~* '^[[:alnum:]_][[:alnum:]_-]+$')
165             )/,
166              
167             q#COMMENT ON TABLE employees IS 'Employee profile associating a real (or imagined) employee with an Employee ID (EID)'#,
168              
169             # 'employees' triggers
170              
171             q/CREATE OR REPLACE FUNCTION eid_immutable() RETURNS trigger AS $IMM$
172             BEGIN
173             IF OLD.eid <> NEW.eid THEN
174             RAISE EXCEPTION 'employees.eid field is immutable';
175             END IF;
176             RETURN NEW;
177             END;
178             $IMM$ LANGUAGE plpgsql/,
179              
180             q/COMMENT ON FUNCTION eid_immutable() IS 'trigger function to prevent users from modifying the EID field'/,
181            
182             q/CREATE TRIGGER no_eid_update BEFORE UPDATE ON employees
183             FOR EACH ROW EXECUTE PROCEDURE eid_immutable()/,
184              
185             q/COMMENT ON TRIGGER no_eid_update ON employees IS 'trigger for eid_immutable()'/,
186              
187             q/CREATE OR REPLACE FUNCTION employee_supervise_self() RETURNS trigger AS $IMM$
188             BEGIN
189             IF NEW.eid = NEW.supervisor THEN
190             RAISE EXCEPTION 'employees cannot be their own supervisor';
191             END IF;
192             RETURN NEW;
193             END;
194             $IMM$ LANGUAGE plpgsql/,
195              
196             q/COMMENT ON FUNCTION employee_supervise_self()
197             IS 'trigger function to prevent employees from supervising themselves'/,
198              
199             q/CREATE TRIGGER no_employee_supervise_self BEFORE INSERT OR UPDATE ON employees
200             FOR EACH ROW EXECUTE PROCEDURE employee_supervise_self()/,
201              
202             q/COMMENT ON TRIGGER no_employee_supervise_self ON employees
203             IS 'Make it impossible for an employee to supervise her- or himself'/,
204              
205             q#-- Given an EID, returns an integer indicating how many "reports"
206             -- the employee has (i.e. how many other employees there are, for whom
207             -- this EID is their supervisor). The integer return value can be used
208             -- as a boolean, too.
209             -- foobar
210             CREATE OR REPLACE FUNCTION has_reports(INTEGER)
211             RETURNS integer AS $$
212             SELECT count(*)::integer FROM employees WHERE supervisor = $1
213             $$ LANGUAGE sql IMMUTABLE#,
214              
215             # the 'schedintvls' table
216              
217             q/CREATE SEQUENCE scratch_sid_seq/,
218              
219             q/COMMENT ON SEQUENCE scratch_sid_seq IS 'sequence guaranteeing that each scratch SID will have a unique identifier'/,
220              
221             q/CREATE TABLE IF NOT EXISTS schedintvls (
222             int_id serial PRIMARY KEY,
223             ssid integer NOT NULL,
224             intvl tstzrange NOT NULL,
225             EXCLUDE USING gist (ssid WITH =, intvl WITH &&)
226             )/,
227              
228             q/COMMENT ON TABLE schedintvls IS $body$
229             Staging table, used to assemble and test schedules before they
230             are converted (using translate_schedintvl) and inserted
231             into the schedules table. Records inserted into schedintvls
232             should be deleted after use.
233             $body$/,
234              
235             q/CREATE TRIGGER schedintvls_valid_intvl BEFORE INSERT OR UPDATE ON schedintvls
236             FOR EACH ROW EXECUTE PROCEDURE valid_intvl()/,
237              
238             q/COMMENT ON TRIGGER schedintvls_valid_intvl ON schedintvls
239             IS 'Run basic validity checks on intervals before they are added to schedintvls table'/,
240              
241             q/CREATE OR REPLACE FUNCTION valid_schedintvl() RETURNS trigger AS $$
242             DECLARE
243             max_upper timestamptz;
244             min_lower timestamptz;
245             BEGIN
246             SELECT MAX(upper(intvl)) FROM (
247             SELECT ssid, intvl FROM schedintvls WHERE schedintvls.ssid = NEW.ssid
248             UNION
249             SELECT NEW.ssid, NEW.intvl
250             ) AS stlasq INTO max_upper;
251             SELECT MIN(lower(intvl)) FROM (
252             SELECT ssid, intvl FROM schedintvls WHERE schedintvls.ssid = NEW.ssid
253             UNION
254             SELECT NEW.ssid, NEW.intvl
255             ) AS stlasq INTO min_lower;
256             IF max_upper - min_lower > '168:0:0' THEN
257             RAISE EXCEPTION 'schedule intervals must fall within a 7-day range';
258             END IF;
259             RETURN NEW;
260             END;
261             $$ LANGUAGE plpgsql IMMUTABLE/,
262              
263             q/COMMENT ON FUNCTION valid_schedintvl() IS $body$
264             trigger function to ensure that all scratch schedule intervals fall within a
265             7-day range
266             $body$
267             /,
268              
269             q/CREATE TRIGGER valid_schedintvl BEFORE INSERT ON schedintvls
270             FOR EACH ROW EXECUTE PROCEDURE valid_schedintvl()/,
271              
272             q/COMMENT ON TRIGGER valid_schedintvl ON schedintvls IS $body$
273             After intervals pass basic validity checks in the schedintvls_valid_intvl
274             trigger, apply schedule-specific checks on the intervals
275             $body$
276             /,
277              
278             q#CREATE OR REPLACE FUNCTION translate_schedintvl (
279             ssid int,
280             OUT low_dow text,
281             OUT low_time text,
282             OUT high_dow text,
283             OUT high_time text
284             ) AS $$
285             SELECT
286             to_char(lower(intvl)::timestamptz, 'DY'),
287             to_char(lower(intvl)::timestamptz, 'HH24:MI'),
288             to_char(upper(intvl)::timestamptz, 'DY'),
289             to_char(upper(intvl)::timestamptz, 'HH24:MI')
290             FROM schedintvls
291             WHERE int_id = $1
292             $$ LANGUAGE sql IMMUTABLE#,
293              
294             q#COMMENT ON FUNCTION translate_schedintvl(ssid int, OUT low_dow text, OUT low_time text, OUT high_dow text, OUT high_time text) IS $body$
295             Given a SSID in schedintvls, returns all the intervals for that
296             SSID. Each interval is expressed as a list ('row', 'composite
297             value') consisting of 4 strings (two pairs). The first pair of
298             strings (e.g., "WED" "08:00") denotes the lower bound of the
299             range, while the second pair denotes the upper bound
300             $body$#,
301              
302             # the 'schedules' table
303              
304             q#CREATE TABLE IF NOT EXISTS schedules (
305             sid serial PRIMARY KEY,
306             scode varchar(32) UNIQUE,
307             schedule text UNIQUE NOT NULL,
308             disabled boolean NOT NULL,
309             remark text
310             )
311             #,
312              
313             q/-- trigger function to detect attempts to change 'schedule' field
314             CREATE OR REPLACE FUNCTION schedule_immutable() RETURNS trigger AS $IMM$
315             BEGIN
316             IF OLD.schedule <> NEW.schedule THEN
317             RAISE EXCEPTION 'schedule field is immutable';
318             END IF;
319             IF OLD.sid <> NEW.sid THEN
320             RAISE EXCEPTION 'schedules.sid field is immutable';
321             END IF;
322             RETURN NEW;
323             END;
324             $IMM$ LANGUAGE plpgsql/,
325            
326             q/-- trigger the trigger
327             CREATE TRIGGER no_schedule_update BEFORE UPDATE ON schedules
328             FOR EACH ROW EXECUTE PROCEDURE schedule_immutable()/,
329            
330             q/-- trigger function to convert NULL to 'f' in boolean field
331             CREATE OR REPLACE FUNCTION disabled_to_zero() RETURNS trigger AS $$
332             BEGIN
333             IF NEW.disabled IS NULL THEN
334             NEW.disabled = 'f';
335             END IF;
336             RETURN NEW;
337             END;
338             $$ LANGUAGE plpgsql IMMUTABLE/,
339              
340             q/-- trigger the disabled_to_zero trigger as well
341             CREATE TRIGGER disabled_to_zero BEFORE INSERT OR UPDATE ON schedules
342             FOR EACH ROW EXECUTE PROCEDURE disabled_to_zero()/,
343              
344             # the 'schedhistory' table
345              
346             q/CREATE TABLE IF NOT EXISTS schedhistory (
347             shid serial PRIMARY KEY,
348             eid integer REFERENCES employees (eid) NOT NULL,
349             sid integer REFERENCES schedules (sid) NOT NULL,
350             effective timestamptz NOT NULL,
351             remark text,
352             UNIQUE (eid, effective)
353             )/,
354              
355             q/-- trigger function to make 'shid' field immutable
356             CREATE OR REPLACE FUNCTION shid_immutable() RETURNS trigger AS $IMM$
357             BEGIN
358             IF OLD.shid <> NEW.shid THEN
359             RAISE EXCEPTION 'schedhistory.shid field is immutable';
360             END IF;
361             RETURN NEW;
362             END;
363             $IMM$ LANGUAGE plpgsql/,
364            
365             q/-- trigger the trigger
366             CREATE TRIGGER no_shid_update BEFORE UPDATE ON schedhistory
367             FOR EACH ROW EXECUTE PROCEDURE shid_immutable()/,
368            
369             # the 'privilege' type
370              
371             q/CREATE TYPE privilege AS ENUM ('passerby', 'inactive', 'active', 'admin')/,
372              
373             # the 'schedhistory' table
374              
375             q/CREATE TABLE IF NOT EXISTS privhistory (
376             phid serial PRIMARY KEY,
377             eid integer REFERENCES employees (eid) NOT NULL,
378             priv privilege NOT NULL,
379             effective timestamptz NOT NULL,
380             remark text,
381             UNIQUE (eid, effective)
382             )/,
383              
384             q/-- trigger function to make 'phid' field immutable
385             CREATE OR REPLACE FUNCTION phid_immutable() RETURNS trigger AS $IMM$
386             BEGIN
387             IF OLD.phid <> NEW.phid THEN
388             RAISE EXCEPTION 'privhistory.phid field is immutable';
389             END IF;
390             RETURN NEW;
391             END;
392             $IMM$ LANGUAGE plpgsql/,
393            
394             q/-- trigger the trigger
395             CREATE TRIGGER no_phid_update BEFORE UPDATE ON privhistory
396             FOR EACH ROW EXECUTE PROCEDURE phid_immutable()/,
397            
398             # triggers shared by 'privhistory' and 'schedhistory'
399              
400             q/CREATE OR REPLACE FUNCTION round_effective() RETURNS trigger AS $$
401             BEGIN
402             NEW.effective = round_time(NEW.effective);
403             RETURN NEW;
404             END;
405             $$ LANGUAGE plpgsql IMMUTABLE/,
406              
407             q/CREATE OR REPLACE FUNCTION sane_timestamp() RETURNS trigger AS $$
408             BEGIN
409             PERFORM not_before_1892(NEW.effective);
410             RETURN NEW;
411             END;
412             $$ LANGUAGE plpgsql IMMUTABLE/,
413              
414             q/CREATE TRIGGER round_effective BEFORE INSERT OR UPDATE ON schedhistory
415             FOR EACH ROW EXECUTE PROCEDURE round_effective()/,
416              
417             q/CREATE TRIGGER round_effective BEFORE INSERT OR UPDATE ON privhistory
418             FOR EACH ROW EXECUTE PROCEDURE round_effective()/,
419              
420             q/CREATE TRIGGER enforce_ts_sanity BEFORE INSERT OR UPDATE ON schedhistory
421             FOR EACH ROW EXECUTE PROCEDURE sane_timestamp()/,
422              
423             q/CREATE TRIGGER enforce_ts_sanity BEFORE INSERT OR UPDATE ON privhistory
424             FOR EACH ROW EXECUTE PROCEDURE sane_timestamp()/,
425              
426             # stored procedures relating to privhistory, schedhistory, and schedule
427              
428             q#-- generalized function to get privilege level for an employee
429             -- as of a given timestamp
430             -- the complicated SELECT is necessary to ensure that the function
431             -- always returns a valid privilege level -- if the EID given doesn't
432             -- have a privilege level for the timestamp given, the function
433             -- returns 'passerby' (for more information, see t/003-current-priv.t)
434             CREATE OR REPLACE FUNCTION priv_at_timestamp (INTEGER, TIMESTAMP WITH TIME ZONE)
435             RETURNS privilege AS $$
436             SELECT priv FROM (
437             SELECT 'passerby' AS priv, '4713-01-01 BC' AS effective
438             UNION
439             SELECT priv, effective FROM privhistory
440             WHERE eid=$1 AND effective <= $2
441             ) AS something_like_a_virtual_table
442             ORDER BY effective DESC
443             FETCH FIRST ROW ONLY
444             $$ LANGUAGE sql IMMUTABLE#,
445              
446             q#-- function to get SID for an employee as of timestamp
447             CREATE OR REPLACE FUNCTION sid_at_timestamp (INTEGER, TIMESTAMP WITH TIME ZONE)
448             RETURNS integer AS $$
449             SELECT sid FROM (
450             SELECT NULL AS sid, '4713-01-01 BC' AS effective
451             UNION
452             SELECT schedules.sid, schedhistory.effective
453             FROM schedules, schedhistory
454             WHERE schedules.sid = schedhistory.sid AND
455             schedhistory.eid=$1 AND
456             schedhistory.effective <= $2
457             ) AS something_like_a_virtual_table
458             ORDER BY effective DESC
459             FETCH FIRST ROW ONLY
460             $$ LANGUAGE sql IMMUTABLE#,
461              
462             q#-- function to get the privhistory record applicable to an employee
463             -- as of a timestamp
464             CREATE OR REPLACE FUNCTION privhistory_at_timestamp (
465             IN INTEGER,
466             IN TIMESTAMP WITH TIME ZONE,
467             OUT phid INTEGER,
468             OUT eid INTEGER,
469             OUT priv PRIVILEGE,
470             OUT effective TIMESTAMP WITH TIME ZONE,
471             OUT remark TEXT
472             )
473             AS $$
474             SELECT phid, eid, priv, effective, remark FROM privhistory
475             WHERE eid=$1 AND effective <= $2
476             ORDER BY effective DESC
477             FETCH FIRST ROW ONLY
478             $$ LANGUAGE sql#,
479              
480             q#-- function to get the privhistory record applicable to an employee
481             -- as of the beginning of a tsrange
482             CREATE OR REPLACE FUNCTION privhistory_at_tsrange (
483             IN INTEGER,
484             IN TSTZRANGE,
485             OUT phid INTEGER,
486             OUT eid INTEGER,
487             OUT priv PRIVILEGE,
488             OUT effective TIMESTAMP WITH TIME ZONE,
489             OUT remark TEXT
490             )
491             AS $$
492             SELECT phid, eid, priv, effective, remark
493             FROM privhistory_at_timestamp( $1, lower( $2 ) )
494             $$ LANGUAGE sql#,
495              
496             q#-- function to get schedhistory record applicable to an employee
497             -- as of a timestamp
498             CREATE OR REPLACE FUNCTION schedhistory_at_timestamp (
499             IN INTEGER,
500             IN TIMESTAMP WITH TIME ZONE,
501             OUT shid INTEGER,
502             OUT eid INTEGER,
503             OUT sid INTEGER,
504             OUT effective TIMESTAMP WITH TIME ZONE,
505             OUT remark TEXT
506             )
507             AS $$
508             SELECT shid, eid, sid, effective, remark FROM schedhistory
509             WHERE eid=$1 AND effective <= $2
510             ORDER BY effective DESC
511             FETCH FIRST ROW ONLY
512             $$ LANGUAGE sql#,
513              
514             q#-- function to get the schedhistory record applicable to an employee
515             -- as of the beginning of a tsrange
516             CREATE OR REPLACE FUNCTION schedhistory_at_tsrange (
517             IN INTEGER,
518             IN TSTZRANGE,
519             OUT shid INTEGER,
520             OUT eid INTEGER,
521             OUT sid INTEGER,
522             OUT effective TIMESTAMP WITH TIME ZONE,
523             OUT remark TEXT
524             )
525             AS $$
526             SELECT shid, eid, sid, effective, remark
527             FROM schedhistory_at_timestamp( $1, lower( $2 ) )
528             $$ LANGUAGE sql#,
529              
530             q#-- Given an EID and a tstzrange, returns a boolean value indicating
531             -- whether or not the employee's privlevel changed during that tstzrange.
532             -- NOTE: history changes lying on an inclusive boundary of the range
533             -- do not trigger a positive!
534             CREATE OR REPLACE FUNCTION priv_change_during_range(INTEGER, TSTZRANGE)
535             RETURNS integer AS $$
536             SELECT count(*)::integer FROM
537             (
538             SELECT
539             $2::tstzrange @> effective
540             AND NOT
541             (
542             ( lower_inc($2::tstzrange) AND effective = lower($2::tstzrange) )
543             OR
544             ( upper_inc($2::tstzrange) AND effective = upper($2::tstzrange) )
545             )
546             AS priv_changed
547             FROM privhistory WHERE eid=$1
548             ) AS tblalias
549             WHERE priv_changed = 't'
550             $$ LANGUAGE sql IMMUTABLE#,
551              
552             q#-- Given an EID and a tstzrange, returns a boolean value indicating
553             -- whether or not the employee's schedule changed during that tstzrange.
554             -- NOTE: history changes lying on an inclusive boundary of the range
555             -- do not trigger a positive!
556             CREATE OR REPLACE FUNCTION schedule_change_during_range(INTEGER, TSTZRANGE)
557             RETURNS integer AS $$
558             SELECT count(*)::integer FROM
559             (
560             SELECT
561             $2::tstzrange @> effective
562             AND NOT
563             (
564             ( lower_inc($2::tstzrange) AND effective = lower($2::tstzrange) )
565             OR
566             ( upper_inc($2::tstzrange) AND effective = upper($2::tstzrange) )
567             )
568             AS schedule_changed
569             FROM schedhistory WHERE eid=$1
570             ) AS tblalias
571             WHERE schedule_changed = 't'
572             $$ LANGUAGE sql IMMUTABLE#,
573              
574             q#-- wrapper function to get priv as of current timestamp
575             CREATE OR REPLACE FUNCTION current_priv (INTEGER)
576             RETURNS privilege AS $$
577             SELECT priv_at_timestamp($1, current_timestamp)
578             $$ LANGUAGE sql IMMUTABLE#,
579              
580             q#-- wrapper function to get schedule as of current timestamp
581             CREATE OR REPLACE FUNCTION current_schedule (INTEGER)
582             RETURNS integer AS $$
583             SELECT sid_at_timestamp($1, current_timestamp)
584             $$ LANGUAGE sql IMMUTABLE#,
585              
586             # the 'activities' table
587              
588             q/-- activities
589             CREATE TABLE activities (
590             aid serial PRIMARY KEY,
591             code varchar(32) UNIQUE NOT NULL,
592             long_desc text,
593             remark text,
594             disabled boolean NOT NULL,
595             stamp json,
596             CONSTRAINT kosher_code CHECK (code ~* '^[[:alnum:]_][[:alnum:]_-]+$')
597             )/,
598            
599             q/-- trigger function to make 'aid' field immutable
600             CREATE OR REPLACE FUNCTION aid_immutable() RETURNS trigger AS $IMM$
601             BEGIN
602             IF OLD.aid <> NEW.aid THEN
603             RAISE EXCEPTION 'activities.aid field is immutable';
604             END IF;
605             RETURN NEW;
606             END;
607             $IMM$ LANGUAGE plpgsql/,
608            
609             q/-- trigger the trigger
610             CREATE TRIGGER no_aid_update BEFORE UPDATE ON activities
611             FOR EACH ROW EXECUTE PROCEDURE aid_immutable()/,
612            
613             q/CREATE OR REPLACE FUNCTION code_to_upper() RETURNS trigger AS $$
614             BEGIN
615             NEW.code = upper(NEW.code);
616             RETURN NEW;
617             END;
618             $$ LANGUAGE plpgsql IMMUTABLE/,
619              
620             q/CREATE TRIGGER code_to_upper BEFORE INSERT OR UPDATE ON activities
621             FOR EACH ROW EXECUTE PROCEDURE code_to_upper()/,
622              
623             q/CREATE TRIGGER disabled_to_zero BEFORE INSERT OR UPDATE ON activities
624             FOR EACH ROW EXECUTE PROCEDURE disabled_to_zero()/,
625              
626             # the 'components' table
627              
628             q#-- components
629             CREATE TABLE components (
630             cid serial PRIMARY KEY,
631             path varchar(2048) UNIQUE NOT NULL,
632             source text NOT NULL,
633             acl varchar(16) NOT NULL,
634             validations text,
635             CONSTRAINT kosher_path CHECK (path ~* '^[[:alnum:]_.][[:alnum:]_/.-]+$')
636             )#,
637            
638             q/-- trigger function to make 'cid' field immutable
639             CREATE OR REPLACE FUNCTION cid_immutable() RETURNS trigger AS $IMM$
640             BEGIN
641             IF OLD.cid <> NEW.cid THEN
642             RAISE EXCEPTION 'components.cid field is immutable';
643             END IF;
644             RETURN NEW;
645             END;
646             $IMM$ LANGUAGE plpgsql/,
647            
648             q/-- trigger the trigger
649             CREATE TRIGGER no_cid_update BEFORE UPDATE ON components
650             FOR EACH ROW EXECUTE PROCEDURE cid_immutable()/,
651            
652             # the 'intervals' table
653              
654             q/-- intervals
655             CREATE TABLE IF NOT EXISTS intervals (
656             iid serial PRIMARY KEY,
657             eid integer REFERENCES employees (eid) NOT NULL,
658             aid integer REFERENCES activities (aid) NOT NULL,
659             intvl tstzrange NOT NULL,
660             long_desc text,
661             remark text,
662             stamp json,
663             EXCLUDE USING gist (eid WITH =, intvl WITH &&)
664             )/,
665              
666             q#-- trigger function to ensure that a privhistory/schedhistory record
667             -- does not fall within an existing attendance interval
668             CREATE OR REPLACE FUNCTION history_policy() RETURNS trigger AS $$
669             DECLARE
670             intvl_count integer;
671             BEGIN
672             -- the EID is NEW.eid, effective timestamptz is NEW.effective
673             SELECT count(*) FROM intervals INTO intvl_count
674             WHERE eid=NEW.eid AND intvl @> NEW.effective;
675             IF intvl_count > 0 THEN
676             RAISE EXCEPTION 'effective timestamp conflicts with existing attendance interval';
677             END IF;
678             RETURN NEW;
679             END;
680             $$ LANGUAGE plpgsql IMMUTABLE#,
681              
682             q/-- trigger the trigger
683             CREATE TRIGGER no_intvl_conflict BEFORE INSERT OR UPDATE ON privhistory
684             FOR EACH ROW EXECUTE PROCEDURE history_policy()/,
685            
686             q/-- trigger the trigger
687             CREATE TRIGGER no_intvl_conflict BEFORE INSERT OR UPDATE ON schedhistory
688             FOR EACH ROW EXECUTE PROCEDURE history_policy()/,
689            
690             q/-- trigger function to enforce policy that an interval cannot come into
691             -- existence unless the employee has only a single privlevel throughout
692             -- the entire interval and that privlevel is either 'active' or 'admin'
693             CREATE OR REPLACE FUNCTION priv_policy() RETURNS trigger AS $$
694             DECLARE
695             priv text;
696             pr_count integer;
697             BEGIN
698             -- the EID is NEW.eid, interval is NEW.intvl
699             -- 1. is there a non-passerbu privilege at the beginning of the interval?
700             SELECT priv_at_timestamp(NEW.eid, lower(NEW.intvl)) INTO priv;
701             IF priv = 'passerby' OR priv = 'inactive' THEN
702             RAISE EXCEPTION 'insufficient privileges: check employee privhistory';
703             END IF;
704             -- 2. are there any privhistory records during the interval?
705             SELECT count(*) FROM privhistory INTO pr_count
706             WHERE eid=NEW.eid AND effective >= lower(NEW.intvl) AND effective <= upper(NEW.intvl);
707             IF pr_count > 0 THEN
708             RAISE EXCEPTION 'ambiguous privilege status: check employee privhistory';
709             END IF;
710             RETURN NEW;
711             END;
712             $$ LANGUAGE plpgsql IMMUTABLE/,
713              
714             q/-- trigger function to enforce policy that an interval cannot come into
715             -- existence unless the employee has only a single schedule throughout
716             -- the entire interval
717             CREATE OR REPLACE FUNCTION schedule_policy() RETURNS trigger AS $$
718             DECLARE
719             test_sid text;
720             sh_count integer;
721             BEGIN
722             -- the EID is NEW.eid, interval is NEW.intvl
723             -- 1. is there a schedule at the beginning of the interval?
724             SELECT sid_at_timestamp(NEW.eid, lower(NEW.intvl)) INTO test_sid;
725             IF test_sid IS NULL THEN
726             RAISE EXCEPTION 'employee schedule for this interval cannot be determined';
727             END IF;
728             -- 2. are there any schedhistory records during the interval?
729             SELECT count(*) FROM schedhistory INTO sh_count
730             WHERE eid=NEW.eid AND effective >= lower(NEW.intvl) AND effective <= upper(NEW.intvl);
731             IF sh_count > 0 THEN
732             RAISE EXCEPTION 'employee schedule for this interval cannot be determined';
733             END IF;
734             RETURN NEW;
735             END;
736             $$ LANGUAGE plpgsql IMMUTABLE/,
737              
738             q/-- trigger function for use in sanity checks on attendance and lock intervals
739             -- vets an interval to ensure it does not extend too far into the future
740             CREATE OR REPLACE FUNCTION not_too_future() RETURNS trigger AS $$
741             DECLARE
742             limit_ts timestamptz;
743             BEGIN
744             --
745             -- does the interval extend too far into the future?
746             --
747             SELECT date_trunc('MONTH', (now() + interval '4 months'))::TIMESTAMPTZ INTO limit_ts;
748             IF upper(NEW.intvl) >= limit_ts THEN
749             RAISE EXCEPTION 'interval extends too far into the future';
750             END IF;
751             RETURN NEW;
752             END;
753             $$ LANGUAGE plpgsql IMMUTABLE/,
754              
755             q/-- trigger function to make 'iid' field immutable
756             CREATE OR REPLACE FUNCTION iid_immutable() RETURNS trigger AS $IMM$
757             BEGIN
758             IF OLD.iid <> NEW.iid THEN
759             RAISE EXCEPTION 'intervals.iid field is immutable';
760             END IF;
761             RETURN NEW;
762             END;
763             $IMM$ LANGUAGE plpgsql/,
764            
765             q/CREATE TRIGGER one_and_only_one_schedule BEFORE INSERT OR UPDATE ON intervals
766             FOR EACH ROW EXECUTE PROCEDURE schedule_policy()/,
767              
768             q/CREATE TRIGGER enforce_priv_policy BEFORE INSERT OR UPDATE ON intervals
769             FOR EACH ROW EXECUTE PROCEDURE priv_policy()/,
770              
771             q/CREATE TRIGGER a1_interval_valid_intvl BEFORE INSERT OR UPDATE ON intervals
772             FOR EACH ROW EXECUTE PROCEDURE valid_intvl()/,
773              
774             q/CREATE TRIGGER a2_interval_not_too_future BEFORE INSERT OR UPDATE ON intervals
775             FOR EACH ROW EXECUTE PROCEDURE not_too_future()/,
776              
777             q/CREATE TRIGGER a3_no_iid_update BEFORE UPDATE ON intervals
778             FOR EACH ROW EXECUTE PROCEDURE iid_immutable()/,
779            
780             # the 'locks' table
781              
782             q/-- locks
783             CREATE TABLE locks (
784             lid serial PRIMARY KEY,
785             eid integer REFERENCES Employees (EID),
786             intvl tstzrange NOT NULL,
787             remark text,
788             stamp json,
789             EXCLUDE USING gist (eid WITH =, intvl WITH &&)
790             )/,
791              
792             q/-- trigger function to make 'lid' field immutable
793             CREATE OR REPLACE FUNCTION lid_immutable() RETURNS trigger AS $IMM$
794             BEGIN
795             IF OLD.lid <> NEW.lid THEN
796             RAISE EXCEPTION 'locks.lid field is immutable';
797             END IF;
798             RETURN NEW;
799             END;
800             $IMM$ LANGUAGE plpgsql/,
801            
802             q/CREATE TRIGGER a1_lock_valid_intvl BEFORE INSERT OR UPDATE ON locks
803             FOR EACH ROW EXECUTE PROCEDURE valid_intvl()/,
804              
805             q/CREATE TRIGGER a2_lock_not_too_future BEFORE INSERT OR UPDATE ON locks
806             FOR EACH ROW EXECUTE PROCEDURE not_too_future()/,
807              
808             q/-- trigger the trigger
809             CREATE TRIGGER a3_no_lid_update BEFORE UPDATE ON locks
810             FOR EACH ROW EXECUTE PROCEDURE lid_immutable()/,
811              
812             q/-- lock lookup trigger for intervals table
813             CREATE OR REPLACE FUNCTION no_lock_conflict() RETURNS trigger AS $IMM$
814             DECLARE
815             this_eid integer;
816             this_intvl tstzrange;
817             lock_count integer;
818             BEGIN
819              
820             IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
821             -- EID and tsrange are NEW.eid and NEW.intvl, respectively
822             this_eid := NEW.eid;
823             this_intvl := NEW.intvl;
824             ELSE
825             -- TG_OP = 'DELETE'
826             this_eid := OLD.eid;
827             this_intvl := OLD.intvl;
828             END IF;
829              
830             SELECT count(*) INTO lock_count FROM locks WHERE eid=this_eid AND intvl && this_intvl;
831             IF lock_count > 0 THEN
832             RAISE EXCEPTION 'interval is locked';
833             END IF;
834              
835             IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
836             RETURN NEW;
837             ELSE
838             RETURN OLD;
839             END IF;
840              
841             END;
842             $IMM$ LANGUAGE plpgsql/,
843            
844             q/CREATE TRIGGER intvl_not_locked BEFORE INSERT OR UPDATE OR DELETE ON intervals
845             FOR EACH ROW EXECUTE PROCEDURE no_lock_conflict()/,
846              
847             # the 'tempintvls' table and associated plumbing
848              
849             q/CREATE SEQUENCE temp_intvl_seq/,
850              
851             q/COMMENT ON SEQUENCE temp_intvl_seq IS 'sequence guaranteeing that each set of temporary intervals will have a unique identifier'/,
852              
853             q/-- tempintvls
854             -- for staging fillup intervals
855             CREATE TABLE IF NOT EXISTS tempintvls (
856             int_id serial PRIMARY KEY,
857             tiid integer NOT NULL,
858             intvl tstzrange NOT NULL
859             )/,
860              
861             q/CREATE TRIGGER a2_interval_not_too_future BEFORE INSERT OR UPDATE ON tempintvls
862             FOR EACH ROW EXECUTE PROCEDURE not_too_future()/,
863              
864             # create 'root' and 'demo' employees
865              
866             q/-- insert root employee into employees table and grant admin
867             -- privilege to the resulting EID
868             WITH cte AS (
869             INSERT INTO employees (nick, fullname, email, passhash, salt, remark)
870             VALUES ('root', 'Root Immutable', 'root@site.org', '82100e9bd4757883b4627b3bafc9389663e7be7f76a1273508a7a617c9dcd917428a7c44c6089477c8e1d13e924343051563d2d426617b695f3a3bff74e7c003', '341755e03e1f163f829785d1d19eab9dee5135c0', 'dbinit')
871             RETURNING eid
872             )
873             INSERT INTO privhistory (eid, priv, effective, remark)
874             SELECT eid, 'admin', '1892-01-01', 'IMMUTABLE' FROM cte
875             /,
876              
877             q/-- insert demo employee into employees table
878             INSERT INTO employees (nick, fullname, email, passhash, salt, remark)
879             VALUES ('demo', 'Demo Employee', 'demo@dochazka.site', '4962cc89c646261a887219795083a02b899ea960cd84a234444b7342e2222eb22dc06f5db9c71681074859469fdc0abd53e3f1f47a381617b59f4b31608e24b1', '82702be8d9810d8fba774dcb7c9f68f39d0933e8', 'dbinit')
880             RETURNING eid
881             /,
882              
883             # DEFAULT schedule
884              
885             q/-- insert DEFAULT schedule into schedules table
886             INSERT INTO schedules (scode, schedule)
887             VALUES ('DEFAULT', '[{"high_dow":"MON","high_time":"12:00","low_dow":"MON","low_time":"08:00"},{"high_dow":"MON","high_time":"16:30","low_dow":"MON","low_time":"12:30"},{"high_dow":"TUE","high_time":"12:00","low_dow":"TUE","low_time":"08:00"},{"high_dow":"TUE","high_time":"16:30","low_dow":"TUE","low_time":"12:30"},{"high_dow":"WED","high_time":"12:00","low_dow":"WED","low_time":"08:00"},{"high_dow":"WED","high_time":"16:30","low_dow":"WED","low_time":"12:30"},{"high_dow":"THU","high_time":"12:00","low_dow":"THU","low_time":"08:00"},{"high_dow":"THU","high_time":"16:30","low_dow":"THU","low_time":"12:30"},{"high_dow":"FRI","high_time":"12:00","low_dow":"FRI","low_time":"08:00"},{"high_dow":"FRI","high_time":"16:30","low_dow":"FRI","low_time":"12:30"}]')
888             /,
889             ]);
890              
891             # DBINIT_SELECT_EID_OF
892             # after create_tables (REST.pm) executes the above list of SQL
893             # statements, it needs to find the EID of the root and demo employees
894             #
895             set('DBINIT_SELECT_EID_OF', q/
896             SELECT eid FROM employees WHERE nick = ?/);
897              
898             # DBINIT_MAKE_ROOT_IMMUTABLE
899             # after finding the EID of the root employee, create_tables executes
900             # another batch of SQL statements to make root immutable
901             # (for more information, see t/002-root.t)
902             #
903             set('DBINIT_MAKE_ROOT_IMMUTABLE', [
904              
905             q/
906             -- trigger function to detect attempts to change nick of the
907             -- root employee
908             CREATE OR REPLACE FUNCTION root_immutable() RETURNS trigger AS $IMM$
909             BEGIN
910             IF OLD.eid = ? THEN
911             IF NEW.eid <> ? THEN
912             RAISE EXCEPTION 'root employee is immutable';
913             END IF;
914             IF NEW.nick <> 'root' THEN
915             RAISE EXCEPTION 'root employee is immutable';
916             END IF;
917             IF NEW.supervisor IS NOT NULL THEN
918             RAISE EXCEPTION 'root employee is immutable';
919             END IF;
920             END IF;
921             RETURN NEW;
922             END;
923             $IMM$ LANGUAGE plpgsql/,
924            
925             q/
926             CREATE OR REPLACE FUNCTION root_immutable_new() RETURNS trigger AS $IMM$
927             BEGIN
928             IF NEW.eid = ? THEN
929             RAISE EXCEPTION 'root employee is immutable';
930             END IF;
931             RETURN NEW;
932             END;
933             $IMM$ LANGUAGE plpgsql/,
934            
935             q/
936             -- for use in BEFORE UPDATE triggers
937             CREATE OR REPLACE FUNCTION root_immutable_old_update() RETURNS trigger AS $IMM$
938             BEGIN
939             IF OLD.eid = ? THEN
940             RAISE EXCEPTION 'root employee is immutable';
941             END IF;
942             RETURN NEW;
943             END;
944             $IMM$ LANGUAGE plpgsql/,
945              
946             q/
947             -- for use in BEFORE DELETE triggers
948             CREATE OR REPLACE FUNCTION root_immutable_old_delete() RETURNS trigger AS $IMM$
949             BEGIN
950             IF OLD.eid = ? THEN
951             RAISE EXCEPTION 'root employee is immutable';
952             END IF;
953             RETURN OLD;
954             END;
955             $IMM$ LANGUAGE plpgsql/,
956              
957             q/
958             -- this trigger makes it impossible to update the root employee
959             CREATE TRIGGER no_root_change BEFORE UPDATE ON employees
960             FOR EACH ROW EXECUTE PROCEDURE root_immutable()/,
961            
962             q/
963             -- this trigger makes it impossible to delete the root employee
964             CREATE TRIGGER no_root_delete BEFORE DELETE ON employees
965             FOR EACH ROW EXECUTE PROCEDURE root_immutable_old_delete()/,
966            
967             q/
968             -- this trigger makes it impossible to introduce any new privhistory
969             -- rows for the root employee
970             CREATE TRIGGER no_root_new BEFORE INSERT OR UPDATE ON privhistory
971             FOR EACH ROW EXECUTE PROCEDURE root_immutable_new()/,
972            
973             q/
974             -- this trigger makes it impossible to update the root
975             -- employee's privhistory row
976             CREATE TRIGGER no_root_update BEFORE UPDATE ON privhistory
977             FOR EACH ROW EXECUTE PROCEDURE root_immutable_old_update()/,
978            
979             q/
980             -- this trigger makes it impossible to delete the root
981             -- employee's privhistory row
982             CREATE TRIGGER no_root_old_delete BEFORE DELETE ON privhistory
983             FOR EACH ROW EXECUTE PROCEDURE root_immutable_old_delete()/,
984            
985             ]);
986              
987             # DBINIT_GRANTS
988             #
989             # whatever GRANT statements we need to do, put them here and they will
990             # get executed after DBINIT_CREATE; ? will be replaced with DOCHAZKA_DBUSER
991             # site param
992             set( 'DBINIT_GRANTS', [
993              
994             q/GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "$dbuser"/,
995              
996             q/GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO "$dbuser"/,
997              
998             q/GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO "$dbuser"/,
999              
1000             q/GRANT CONNECT ON DATABASE "$dbname" TO "$dbuser"/,
1001              
1002             ] );
1003              
1004             # SQL_NOOF_CONNECTIONS
1005             # used by 'GET dbstatus'
1006             #
1007             set('SQL_NOOF_CONNECTIONS', q/SELECT sum(numbackends) FROM pg_stat_database/);
1008             #set('SQL_NOOF_CONNECTIONS', q/SELECT count(*) FROM pg_stat_activity/);
1009              
1010             # -----------------------------------
1011             # DO NOT EDIT ANYTHING BELOW THIS LINE
1012             # -----------------------------------
1013 41     41   48356 use strict;
  41         102  
  41         1085  
1014 41     41   271 use warnings;
  41         116  
  41         1509  
1015              
1016             1;