]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/090.schema.action.sql
Add a trigger function to simulate FKEY constraints on inherited tables
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 090.schema.action.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2007-2008  Equinox Software, Inc.
4  * Mike Rylander <miker@esilibrary.com> 
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  */
17
18 DROP SCHEMA IF EXISTS action CASCADE;
19
20 BEGIN;
21
22 CREATE SCHEMA action;
23
24 CREATE TABLE action.in_house_use (
25         id              SERIAL                          PRIMARY KEY,
26         item            BIGINT                          NOT NULL, -- REFERENCES asset.copy (id) DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
27         staff           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
28         org_unit        INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
29         use_time        TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW()
30 );
31 CREATE INDEX action_in_house_use_staff_idx      ON action.in_house_use ( staff );
32
33 CREATE TABLE action.non_cataloged_circulation (
34         id              SERIAL                          PRIMARY KEY,
35         patron          INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
36         staff           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
37         circ_lib        INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
38         item_type       INT                             NOT NULL REFERENCES config.non_cataloged_type (id) DEFERRABLE INITIALLY DEFERRED,
39         circ_time       TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW()
40 );
41 CREATE INDEX action_non_cat_circ_patron_idx ON action.non_cataloged_circulation ( patron );
42 CREATE INDEX action_non_cat_circ_staff_idx  ON action.non_cataloged_circulation ( staff );
43
44 CREATE TABLE action.non_cat_in_house_use (
45         id              SERIAL                          PRIMARY KEY,
46         item_type       BIGINT                          NOT NULL REFERENCES config.non_cataloged_type(id) DEFERRABLE INITIALLY DEFERRED,
47         staff           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
48         org_unit        INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
49         use_time        TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW()
50 );
51 CREATE INDEX non_cat_in_house_use_staff_idx ON action.non_cat_in_house_use ( staff );
52
53 CREATE TABLE action.survey (
54         id              SERIAL                          PRIMARY KEY,
55         owner           INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
56         start_date      TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
57         end_date        TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW() + '10 years'::INTERVAL,
58         usr_summary     BOOL                            NOT NULL DEFAULT FALSE,
59         opac            BOOL                            NOT NULL DEFAULT FALSE,
60         poll            BOOL                            NOT NULL DEFAULT FALSE,
61         required        BOOL                            NOT NULL DEFAULT FALSE,
62         name            TEXT                            NOT NULL,
63         description     TEXT                            NOT NULL
64 );
65 CREATE UNIQUE INDEX asv_once_per_owner_idx ON action.survey (owner,name);
66
67 CREATE TABLE action.survey_question (
68         id              SERIAL  PRIMARY KEY,
69         survey          INT     NOT NULL REFERENCES action.survey DEFERRABLE INITIALLY DEFERRED,
70         question        TEXT    NOT NULL
71 );
72
73 CREATE TABLE action.survey_answer (
74         id              SERIAL  PRIMARY KEY,
75         question        INT     NOT NULL REFERENCES action.survey_question DEFERRABLE INITIALLY DEFERRED,
76         answer          TEXT    NOT NULL
77 );
78
79 CREATE SEQUENCE action.survey_response_group_id_seq;
80
81 CREATE TABLE action.survey_response (
82         id                      BIGSERIAL                       PRIMARY KEY,
83         response_group_id       INT,
84         usr                     INT, -- REFERENCES actor.usr
85         survey                  INT                             NOT NULL REFERENCES action.survey DEFERRABLE INITIALLY DEFERRED,
86         question                INT                             NOT NULL REFERENCES action.survey_question DEFERRABLE INITIALLY DEFERRED,
87         answer                  INT                             NOT NULL REFERENCES action.survey_answer DEFERRABLE INITIALLY DEFERRED,
88         answer_date             TIMESTAMP WITH TIME ZONE,
89         effective_date          TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW()
90 );
91 CREATE INDEX action_survey_response_usr_idx ON action.survey_response ( usr );
92
93 CREATE OR REPLACE FUNCTION action.survey_response_answer_date_fixup () RETURNS TRIGGER AS '
94 BEGIN
95         NEW.answer_date := NOW();
96         RETURN NEW;
97 END;
98 ' LANGUAGE 'plpgsql';
99 CREATE TRIGGER action_survey_response_answer_date_fixup_tgr
100         BEFORE INSERT ON action.survey_response
101         FOR EACH ROW
102         EXECUTE PROCEDURE action.survey_response_answer_date_fixup ();
103
104
105 CREATE TABLE action.circulation (
106         target_copy             BIGINT                          NOT NULL, -- asset.copy.id
107         circ_lib                INT                             NOT NULL, -- actor.org_unit.id
108         circ_staff              INT                             NOT NULL, -- actor.usr.id
109         checkin_staff           INT,                                      -- actor.usr.id
110         checkin_lib             INT,                                      -- actor.org_unit.id
111         renewal_remaining       INT                             NOT NULL, -- derived from "circ duration" rule
112     grace_period           INTERVAL             NOT NULL, -- derived from "circ fine" rule
113         due_date                TIMESTAMP WITH TIME ZONE,
114         stop_fines_time         TIMESTAMP WITH TIME ZONE,
115         checkin_time            TIMESTAMP WITH TIME ZONE,
116         create_time             TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
117         duration                INTERVAL,                                 -- derived from "circ duration" rule
118         fine_interval           INTERVAL                        NOT NULL DEFAULT '1 day'::INTERVAL, -- derived from "circ fine" rule
119         recurring_fine          NUMERIC(6,2),                             -- derived from "circ fine" rule
120         max_fine                NUMERIC(6,2),                             -- derived from "max fine" rule
121         phone_renewal           BOOL                            NOT NULL DEFAULT FALSE,
122         desk_renewal            BOOL                            NOT NULL DEFAULT FALSE,
123         opac_renewal            BOOL                            NOT NULL DEFAULT FALSE,
124         duration_rule           TEXT                            NOT NULL, -- name of "circ duration" rule
125         recurring_fine_rule     TEXT                            NOT NULL, -- name of "circ fine" rule
126         max_fine_rule           TEXT                            NOT NULL, -- name of "max fine" rule
127         stop_fines              TEXT                            CHECK (stop_fines IN (
128                                                'CHECKIN','CLAIMSRETURNED','LOST','MAXFINES','RENEW','LONGOVERDUE','CLAIMSNEVERCHECKEDOUT')),
129         workstation         INT        REFERENCES actor.workstation(id)
130                                        ON DELETE SET NULL
131                                                                    DEFERRABLE INITIALLY DEFERRED,
132         checkin_workstation INT        REFERENCES actor.workstation(id)
133                                        ON DELETE SET NULL
134                                                                    DEFERRABLE INITIALLY DEFERRED,
135         checkin_scan_time   TIMESTAMP WITH TIME ZONE
136 ) INHERITS (money.billable_xact);
137 ALTER TABLE action.circulation ADD PRIMARY KEY (id);
138 ALTER TABLE action.circulation
139         ADD COLUMN parent_circ BIGINT
140         REFERENCES action.circulation( id )
141         DEFERRABLE INITIALLY DEFERRED;
142 CREATE INDEX circ_open_xacts_idx ON action.circulation (usr) WHERE xact_finish IS NULL;
143 CREATE INDEX circ_outstanding_idx ON action.circulation (usr) WHERE checkin_time IS NULL;
144 CREATE INDEX circ_checkin_time ON "action".circulation (checkin_time) WHERE checkin_time IS NOT NULL;
145 CREATE INDEX circ_circ_lib_idx ON "action".circulation (circ_lib);
146 CREATE INDEX circ_open_date_idx ON "action".circulation (xact_start) WHERE xact_finish IS NULL;
147 CREATE INDEX circ_all_usr_idx       ON action.circulation ( usr );
148 CREATE INDEX circ_circ_staff_idx    ON action.circulation ( circ_staff );
149 CREATE INDEX circ_checkin_staff_idx ON action.circulation ( checkin_staff );
150 CREATE INDEX action_circulation_target_copy_idx ON action.circulation (target_copy);
151 CREATE UNIQUE INDEX circ_parent_idx ON action.circulation ( parent_circ ) WHERE parent_circ IS NOT NULL;
152 CREATE UNIQUE INDEX only_one_concurrent_checkout_per_copy ON action.circulation(target_copy) WHERE checkin_time IS NULL;
153
154 CREATE TRIGGER action_circulation_target_copy_trig AFTER INSERT OR UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE evergreen.fake_fkey_tgr('target_copy');
155
156 CREATE TRIGGER mat_summary_create_tgr AFTER INSERT ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_create ('circulation');
157 CREATE TRIGGER mat_summary_change_tgr AFTER UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_update ();
158 CREATE TRIGGER mat_summary_remove_tgr AFTER DELETE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_delete ();
159
160 CREATE OR REPLACE FUNCTION action.push_circ_due_time () RETURNS TRIGGER AS $$
161 BEGIN
162     IF (EXTRACT(EPOCH FROM NEW.duration)::INT % EXTRACT(EPOCH FROM '1 day'::INTERVAL)::INT) = 0 THEN
163         NEW.due_date = (NEW.due_date::DATE + '1 day'::INTERVAL - '1 second'::INTERVAL)::TIMESTAMPTZ;
164     END IF;
165
166     RETURN NEW;
167 END;
168 $$ LANGUAGE PLPGSQL;
169
170 CREATE TRIGGER push_due_date_tgr BEFORE INSERT OR UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE action.push_circ_due_time();
171
172 CREATE TABLE action.aged_circulation (
173         usr_post_code           TEXT,
174         usr_home_ou             INT     NOT NULL,
175         usr_profile             INT     NOT NULL,
176         usr_birth_year          INT,
177         copy_call_number        INT     NOT NULL,
178         copy_location           INT     NOT NULL,
179         copy_owning_lib         INT     NOT NULL,
180         copy_circ_lib           INT     NOT NULL,
181         copy_bib_record         BIGINT  NOT NULL,
182         LIKE action.circulation
183
184 );
185 ALTER TABLE action.aged_circulation ADD PRIMARY KEY (id);
186 ALTER TABLE action.aged_circulation DROP COLUMN usr;
187 CREATE INDEX aged_circ_circ_lib_idx ON "action".aged_circulation (circ_lib);
188 CREATE INDEX aged_circ_start_idx ON "action".aged_circulation (xact_start);
189 CREATE INDEX aged_circ_copy_circ_lib_idx ON "action".aged_circulation (copy_circ_lib);
190 CREATE INDEX aged_circ_copy_owning_lib_idx ON "action".aged_circulation (copy_owning_lib);
191 CREATE INDEX aged_circ_copy_location_idx ON "action".aged_circulation (copy_location);
192 CREATE INDEX action_aged_circulation_target_copy_idx ON action.aged_circulation (target_copy);
193
194 CREATE OR REPLACE VIEW action.all_circulation AS
195     SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
196         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
197         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
198         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
199         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
200         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
201       FROM  action.aged_circulation
202             UNION ALL
203     SELECT  DISTINCT circ.id,COALESCE(a.post_code,b.post_code) AS usr_post_code, p.home_ou AS usr_home_ou, p.profile AS usr_profile, EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
204         cp.call_number AS copy_call_number, cp.location AS copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
205         cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
206         circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
207         circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
208         circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
209         circ.parent_circ
210       FROM  action.circulation circ
211         JOIN asset.copy cp ON (circ.target_copy = cp.id)
212         JOIN asset.call_number cn ON (cp.call_number = cn.id)
213         JOIN actor.usr p ON (circ.usr = p.id)
214         LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
215         LEFT JOIN actor.usr_address b ON (p.billing_address = a.id);
216
217 CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
218 DECLARE
219 found char := 'N';
220 BEGIN
221
222     -- If there are any renewals for this circulation, don't archive or delete
223     -- it yet.   We'll do so later, when we archive and delete the renewals.
224
225     SELECT 'Y' INTO found
226     FROM action.circulation
227     WHERE parent_circ = OLD.id
228     LIMIT 1;
229
230     IF found = 'Y' THEN
231         RETURN NULL;  -- don't delete
232         END IF;
233
234     -- Archive a copy of the old row to action.aged_circulation
235
236     INSERT INTO action.aged_circulation
237         (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
238         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
239         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
240         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
241         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
242         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
243       SELECT
244         id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
245         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
246         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
247         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
248         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
249         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
250         FROM action.all_circulation WHERE id = OLD.id;
251
252     RETURN OLD;
253 END;
254 $$ LANGUAGE 'plpgsql';
255
256 CREATE TRIGGER action_circulation_aging_tgr
257         BEFORE DELETE ON action.circulation
258         FOR EACH ROW
259         EXECUTE PROCEDURE action.age_circ_on_delete ();
260
261
262 CREATE OR REPLACE FUNCTION action.age_parent_circ_on_delete () RETURNS TRIGGER AS $$
263 BEGIN
264
265     -- Having deleted a renewal, we can delete the original circulation (or a previous
266     -- renewal, if that's what parent_circ is pointing to).  That deletion will trigger
267     -- deletion of any prior parents, etc. recursively.
268
269     IF OLD.parent_circ IS NOT NULL THEN
270         DELETE FROM action.circulation
271         WHERE id = OLD.parent_circ;
272     END IF;
273
274     RETURN OLD;
275 END;
276 $$ LANGUAGE 'plpgsql';
277
278 CREATE TRIGGER age_parent_circ
279         AFTER DELETE ON action.circulation
280         FOR EACH ROW
281         EXECUTE PROCEDURE action.age_parent_circ_on_delete ();
282
283
284 CREATE OR REPLACE VIEW action.open_circulation AS
285         SELECT  *
286           FROM  action.circulation
287           WHERE checkin_time IS NULL
288           ORDER BY due_date;
289                 
290
291 CREATE OR REPLACE VIEW action.billable_circulations AS
292         SELECT  *
293           FROM  action.circulation
294           WHERE xact_finish IS NULL;
295
296 CREATE OR REPLACE FUNCTION action.circulation_claims_returned () RETURNS TRIGGER AS $$
297 BEGIN
298         IF OLD.stop_fines IS NULL OR OLD.stop_fines <> NEW.stop_fines THEN
299                 IF NEW.stop_fines = 'CLAIMSRETURNED' THEN
300                         UPDATE actor.usr SET claims_returned_count = claims_returned_count + 1 WHERE id = NEW.usr;
301                 END IF;
302                 IF NEW.stop_fines = 'CLAIMSNEVERCHECKEDOUT' THEN
303                         UPDATE actor.usr SET claims_never_checked_out_count = claims_never_checked_out_count + 1 WHERE id = NEW.usr;
304                 END IF;
305                 IF NEW.stop_fines = 'LOST' THEN
306                         UPDATE asset.copy SET status = 3 WHERE id = NEW.target_copy;
307                 END IF;
308         END IF;
309         RETURN NEW;
310 END;
311 $$ LANGUAGE 'plpgsql';
312 CREATE TRIGGER action_circulation_stop_fines_tgr
313         BEFORE UPDATE ON action.circulation
314         FOR EACH ROW
315         EXECUTE PROCEDURE action.circulation_claims_returned ();
316
317 CREATE TABLE action.hold_request_cancel_cause (
318     id      SERIAL  PRIMARY KEY,
319     label   TEXT    UNIQUE
320 );
321 INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (1,'Untargeted expiration');
322 INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (2,'Hold Shelf expiration');
323 INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (3,'Patron via phone');
324 INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (4,'Patron in person');
325 INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (5,'Staff forced');
326 INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (6,'Patron via OPAC');
327 SELECT SETVAL('action.hold_request_cancel_cause_id_seq', 100);
328
329 CREATE TABLE action.hold_request (
330         id                      SERIAL                          PRIMARY KEY,
331         request_time            TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
332         capture_time            TIMESTAMP WITH TIME ZONE,
333         fulfillment_time        TIMESTAMP WITH TIME ZONE,
334         checkin_time            TIMESTAMP WITH TIME ZONE,
335         return_time             TIMESTAMP WITH TIME ZONE,
336         prev_check_time         TIMESTAMP WITH TIME ZONE,
337         expire_time             TIMESTAMP WITH TIME ZONE,
338         cancel_time             TIMESTAMP WITH TIME ZONE,
339         cancel_cause    INT REFERENCES action.hold_request_cancel_cause (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
340         cancel_note             TEXT,
341         target                  BIGINT                          NOT NULL, -- see hold_type
342         current_copy            BIGINT,                         -- REFERENCES asset.copy (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,  -- XXX could be an serial.unit now...
343         fulfillment_staff       INT                             REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
344         fulfillment_lib         INT                             REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
345         request_lib             INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
346         requestor               INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
347         usr                     INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
348         selection_ou            INT                             NOT NULL,
349         selection_depth         INT                             NOT NULL DEFAULT 0,
350         pickup_lib              INT                             NOT NULL REFERENCES actor.org_unit DEFERRABLE INITIALLY DEFERRED,
351         hold_type               TEXT                            NOT NULL, -- CHECK (hold_type IN ('M','T','V','C')),  -- XXX constraint too constraining...
352         holdable_formats        TEXT,
353         phone_notify            TEXT,
354         email_notify            BOOL                            NOT NULL DEFAULT TRUE,
355         frozen                  BOOL                            NOT NULL DEFAULT FALSE,
356         thaw_date               TIMESTAMP WITH TIME ZONE,
357         shelf_time              TIMESTAMP WITH TIME ZONE,
358     cut_in_line     BOOL,
359         mint_condition  BOOL NOT NULL DEFAULT TRUE,
360         shelf_expire_time TIMESTAMPTZ
361 );
362
363 CREATE INDEX hold_request_target_idx ON action.hold_request (target);
364 CREATE INDEX hold_request_usr_idx ON action.hold_request (usr);
365 CREATE INDEX hold_request_pickup_lib_idx ON action.hold_request (pickup_lib);
366 CREATE INDEX hold_request_current_copy_idx ON action.hold_request (current_copy);
367 CREATE INDEX hold_request_prev_check_time_idx ON action.hold_request (prev_check_time);
368 CREATE INDEX hold_request_fulfillment_staff_idx ON action.hold_request ( fulfillment_staff );
369 CREATE INDEX hold_request_requestor_idx         ON action.hold_request ( requestor );
370
371
372 CREATE TABLE action.hold_request_note (
373
374     id     BIGSERIAL PRIMARY KEY,
375     hold   BIGINT    NOT NULL REFERENCES action.hold_request (id)
376                               ON DELETE CASCADE
377                               DEFERRABLE INITIALLY DEFERRED,
378     title  TEXT      NOT NULL,
379     body   TEXT      NOT NULL,
380     slip   BOOL      NOT NULL DEFAULT FALSE,
381     pub    BOOL      NOT NULL DEFAULT FALSE,
382     staff  BOOL      NOT NULL DEFAULT FALSE  -- created by staff
383
384 );
385 CREATE INDEX ahrn_hold_idx ON action.hold_request_note (hold);
386
387
388 CREATE TABLE action.hold_notification (
389         id              SERIAL                          PRIMARY KEY,
390         hold            INT                             NOT NULL REFERENCES action.hold_request (id)
391                                                                         ON DELETE CASCADE
392                                                                         DEFERRABLE INITIALLY DEFERRED,
393         notify_staff    INT                     REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
394         notify_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
395         method          TEXT                            NOT NULL, -- email address or phone number
396         note            TEXT
397 );
398 CREATE INDEX ahn_hold_idx ON action.hold_notification (hold);
399 CREATE INDEX ahn_notify_staff_idx ON action.hold_notification ( notify_staff );
400
401 CREATE TABLE action.hold_copy_map (
402         id              BIGSERIAL       PRIMARY KEY,
403         hold            INT     NOT NULL REFERENCES action.hold_request (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
404         target_copy     BIGINT  NOT NULL, -- REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
405         CONSTRAINT copy_once_per_hold UNIQUE (hold,target_copy)
406 );
407 -- CREATE INDEX acm_hold_idx ON action.hold_copy_map (hold);
408 CREATE INDEX acm_copy_idx ON action.hold_copy_map (target_copy);
409
410 CREATE TABLE action.transit_copy (
411         id                      SERIAL                          PRIMARY KEY,
412         source_send_time        TIMESTAMP WITH TIME ZONE,
413         dest_recv_time          TIMESTAMP WITH TIME ZONE,
414         target_copy             BIGINT                          NOT NULL, -- REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
415         source                  INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
416         dest                    INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
417         prev_hop                INT                             REFERENCES action.transit_copy (id) DEFERRABLE INITIALLY DEFERRED,
418         copy_status             INT                             NOT NULL REFERENCES config.copy_status (id) DEFERRABLE INITIALLY DEFERRED,
419         persistant_transfer     BOOL                            NOT NULL DEFAULT FALSE,
420         prev_dest       INT                             REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED
421 );
422 CREATE INDEX active_transit_dest_idx ON "action".transit_copy (dest); 
423 CREATE INDEX active_transit_source_idx ON "action".transit_copy (source);
424 CREATE INDEX active_transit_cp_idx ON "action".transit_copy (target_copy);
425
426
427 CREATE TABLE action.hold_transit_copy (
428         hold    INT     REFERENCES action.hold_request (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
429 ) INHERITS (action.transit_copy);
430 ALTER TABLE action.hold_transit_copy ADD PRIMARY KEY (id);
431 -- ALTER TABLE action.hold_transit_copy ADD CONSTRAINT ahtc_tc_fkey FOREIGN KEY (target_copy) REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; -- XXX could be an serial.issuance
432 CREATE INDEX active_hold_transit_dest_idx ON "action".hold_transit_copy (dest);
433 CREATE INDEX active_hold_transit_source_idx ON "action".hold_transit_copy (source);
434 CREATE INDEX active_hold_transit_cp_idx ON "action".hold_transit_copy (target_copy);
435
436
437 CREATE TABLE action.unfulfilled_hold_list (
438         id              BIGSERIAL                       PRIMARY KEY,
439         current_copy    BIGINT                          NOT NULL,
440         hold            INT                             NOT NULL,
441         circ_lib        INT                             NOT NULL,
442         fail_time       TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW()
443 );
444 CREATE INDEX uhr_hold_idx ON action.unfulfilled_hold_list (hold);
445
446 CREATE OR REPLACE VIEW action.unfulfilled_hold_loops AS
447     SELECT  u.hold,
448             c.circ_lib,
449             count(*)
450       FROM  action.unfulfilled_hold_list u
451             JOIN asset.copy c ON (c.id = u.current_copy)
452       GROUP BY 1,2;
453
454 CREATE OR REPLACE VIEW action.unfulfilled_hold_min_loop AS
455     SELECT  hold,
456             min(count)
457       FROM  action.unfulfilled_hold_loops
458       GROUP BY 1;
459
460 CREATE OR REPLACE VIEW action.unfulfilled_hold_innermost_loop AS
461     SELECT  DISTINCT l.*
462       FROM  action.unfulfilled_hold_loops l
463             JOIN action.unfulfilled_hold_min_loop m USING (hold)
464       WHERE l.count = m.min;
465
466 CREATE VIEW action.unfulfilled_hold_max_loop AS
467     SELECT  hold,
468             max(count) AS max
469       FROM  action.unfulfilled_hold_loops
470       GROUP BY 1;
471
472
473 CREATE TABLE action.fieldset (
474     id              SERIAL          PRIMARY KEY,
475     owner           INT             NOT NULL REFERENCES actor.usr (id)
476                                     DEFERRABLE INITIALLY DEFERRED,
477         owning_lib      INT             NOT NULL REFERENCES actor.org_unit (id)
478                                     DEFERRABLE INITIALLY DEFERRED,
479         status          TEXT            NOT NULL
480                                         CONSTRAINT valid_status CHECK ( status in
481                                                                         ( 'PENDING', 'APPLIED', 'ERROR' )),
482     creation_time   TIMESTAMPTZ     NOT NULL DEFAULT NOW(),
483     scheduled_time  TIMESTAMPTZ,
484     applied_time    TIMESTAMPTZ,
485     classname       TEXT            NOT NULL, -- an IDL class name
486     name            TEXT            NOT NULL,
487     stored_query    INT             REFERENCES query.stored_query (id)
488                                     DEFERRABLE INITIALLY DEFERRED,
489     pkey_value      TEXT,
490         CONSTRAINT lib_name_unique UNIQUE (owning_lib, name),
491     CONSTRAINT fieldset_one_or_the_other CHECK (
492         (stored_query IS NOT NULL AND pkey_value IS NULL) OR
493         (pkey_value IS NOT NULL AND stored_query IS NULL)
494     )
495         -- the CHECK constraint means we can update the fields for a single
496         -- row without all the extra overhead involved in a query
497 );
498
499 CREATE INDEX action_fieldset_sched_time_idx ON action.fieldset( scheduled_time );
500 CREATE INDEX action_owner_idx               ON action.fieldset( owner );
501
502
503 CREATE TABLE action.fieldset_col_val (
504     id              SERIAL  PRIMARY KEY,
505     fieldset        INT     NOT NULL REFERENCES action.fieldset
506                                          ON DELETE CASCADE
507                                          DEFERRABLE INITIALLY DEFERRED,
508     col             TEXT    NOT NULL,  -- "field" from the idl ... the column on the table
509     val             TEXT,              -- value for the column ... NULL means, well, NULL
510     CONSTRAINT fieldset_col_once_per_set UNIQUE (fieldset, col)
511 );
512
513
514 -- represents a circ chain summary
515 CREATE TYPE action.circ_chain_summary AS (
516     num_circs INTEGER,
517     start_time TIMESTAMP WITH TIME ZONE,
518     checkout_workstation TEXT,
519     last_renewal_time TIMESTAMP WITH TIME ZONE, -- NULL if no renewals
520     last_stop_fines TEXT,
521     last_stop_fines_time TIMESTAMP WITH TIME ZONE,
522     last_renewal_workstation TEXT, -- NULL if no renewals
523     last_checkin_workstation TEXT,
524     last_checkin_time TIMESTAMP WITH TIME ZONE,
525     last_checkin_scan_time TIMESTAMP WITH TIME ZONE
526 );
527
528
529 CREATE OR REPLACE FUNCTION action.circ_chain ( ctx_circ_id INTEGER ) RETURNS SETOF action.circulation AS $$
530 DECLARE
531     tmp_circ action.circulation%ROWTYPE;
532     circ_0 action.circulation%ROWTYPE;
533 BEGIN
534
535     SELECT INTO tmp_circ * FROM action.circulation WHERE id = ctx_circ_id;
536
537     IF tmp_circ IS NULL THEN
538         RETURN NEXT tmp_circ;
539     END IF;
540     circ_0 := tmp_circ;
541
542     -- find the front of the chain
543     WHILE TRUE LOOP
544         SELECT INTO tmp_circ * FROM action.circulation WHERE id = tmp_circ.parent_circ;
545         IF tmp_circ IS NULL THEN
546             EXIT;
547         END IF;
548         circ_0 := tmp_circ;
549     END LOOP;
550
551     -- now send the circs to the caller, oldest to newest
552     tmp_circ := circ_0;
553     WHILE TRUE LOOP
554         IF tmp_circ IS NULL THEN
555             EXIT;
556         END IF;
557         RETURN NEXT tmp_circ;
558         SELECT INTO tmp_circ * FROM action.circulation WHERE parent_circ = tmp_circ.id;
559     END LOOP;
560
561 END;
562 $$ LANGUAGE 'plpgsql';
563
564 CREATE OR REPLACE FUNCTION action.summarize_circ_chain ( ctx_circ_id INTEGER ) RETURNS action.circ_chain_summary AS $$
565
566 DECLARE
567
568     -- first circ in the chain
569     circ_0 action.circulation%ROWTYPE;
570
571     -- last circ in the chain
572     circ_n action.circulation%ROWTYPE;
573
574     -- circ chain under construction
575     chain action.circ_chain_summary;
576     tmp_circ action.circulation%ROWTYPE;
577
578 BEGIN
579     
580     chain.num_circs := 0;
581     FOR tmp_circ IN SELECT * FROM action.circ_chain(ctx_circ_id) LOOP
582
583         IF chain.num_circs = 0 THEN
584             circ_0 := tmp_circ;
585         END IF;
586
587         chain.num_circs := chain.num_circs + 1;
588         circ_n := tmp_circ;
589     END LOOP;
590
591     chain.start_time := circ_0.xact_start;
592     chain.last_stop_fines := circ_n.stop_fines;
593     chain.last_stop_fines_time := circ_n.stop_fines_time;
594     chain.last_checkin_time := circ_n.checkin_time;
595     chain.last_checkin_scan_time := circ_n.checkin_scan_time;
596     SELECT INTO chain.checkout_workstation name FROM actor.workstation WHERE id = circ_0.workstation;
597     SELECT INTO chain.last_checkin_workstation name FROM actor.workstation WHERE id = circ_n.checkin_workstation;
598
599     IF chain.num_circs > 1 THEN
600         chain.last_renewal_time := circ_n.xact_start;
601         SELECT INTO chain.last_renewal_workstation name FROM actor.workstation WHERE id = circ_n.workstation;
602     END IF;
603
604     RETURN chain;
605
606 END;
607 $$ LANGUAGE 'plpgsql';
608
609 -- Return the list of circ chain heads in xact_start order that the user has chosen to "retain"
610 CREATE OR REPLACE FUNCTION action.usr_visible_circs (usr_id INT) RETURNS SETOF action.circulation AS $func$
611 DECLARE
612     c               action.circulation%ROWTYPE;
613     view_age        INTERVAL;
614     usr_view_age    actor.usr_setting%ROWTYPE;
615     usr_view_start  actor.usr_setting%ROWTYPE;
616 BEGIN
617     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_age';
618     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_start';
619
620     IF usr_view_age.value IS NOT NULL AND usr_view_start.value IS NOT NULL THEN
621         -- User opted in and supplied a retention age
622         IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
623             view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
624         ELSE
625             view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
626         END IF;
627     ELSIF usr_view_start.value IS NOT NULL THEN
628         -- User opted in
629         view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
630     ELSE
631         -- User did not opt in
632         RETURN;
633     END IF;
634
635     FOR c IN
636         SELECT  *
637           FROM  action.circulation
638           WHERE usr = usr_id
639                 AND parent_circ IS NULL
640                 AND xact_start > NOW() - view_age
641           ORDER BY xact_start
642     LOOP
643         RETURN NEXT c;
644     END LOOP;
645
646     RETURN;
647 END;
648 $func$ LANGUAGE PLPGSQL;
649
650 CREATE OR REPLACE FUNCTION action.usr_visible_circ_copies( INTEGER ) RETURNS SETOF BIGINT AS $$
651     SELECT DISTINCT(target_copy) FROM action.usr_visible_circs($1)
652 $$ LANGUAGE SQL;
653
654 CREATE OR REPLACE FUNCTION action.usr_visible_holds (usr_id INT) RETURNS SETOF action.hold_request AS $func$
655 DECLARE
656     h               action.hold_request%ROWTYPE;
657     view_age        INTERVAL;
658     view_count      INT;
659     usr_view_count  actor.usr_setting%ROWTYPE;
660     usr_view_age    actor.usr_setting%ROWTYPE;
661     usr_view_start  actor.usr_setting%ROWTYPE;
662 BEGIN
663     SELECT * INTO usr_view_count FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_count';
664     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_age';
665     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_start';
666
667     FOR h IN
668         SELECT  *
669           FROM  action.hold_request
670           WHERE usr = usr_id
671                 AND fulfillment_time IS NULL
672                 AND cancel_time IS NULL
673           ORDER BY request_time DESC
674     LOOP
675         RETURN NEXT h;
676     END LOOP;
677
678     IF usr_view_start.value IS NULL THEN
679         RETURN;
680     END IF;
681
682     IF usr_view_age.value IS NOT NULL THEN
683         -- User opted in and supplied a retention age
684         IF oils_json_to_string(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_string(usr_view_start.value)::TIMESTAMPTZ) THEN
685             view_age := AGE(NOW(), oils_json_to_string(usr_view_start.value)::TIMESTAMPTZ);
686         ELSE
687             view_age := oils_json_to_string(usr_view_age.value)::INTERVAL;
688         END IF;
689     ELSE
690         -- User opted in
691         view_age := AGE(NOW(), oils_json_to_string(usr_view_start.value)::TIMESTAMPTZ);
692     END IF;
693
694     IF usr_view_count.value IS NOT NULL THEN
695         view_count := oils_json_to_text(usr_view_count.value)::INT;
696     ELSE
697         view_count := 1000;
698     END IF;
699
700     -- show some fulfilled/canceled holds
701     FOR h IN
702         SELECT  *
703           FROM  action.hold_request
704           WHERE usr = usr_id
705                 AND ( fulfillment_time IS NOT NULL OR cancel_time IS NOT NULL )
706                 AND request_time > NOW() - view_age
707           ORDER BY request_time DESC
708           LIMIT view_count
709     LOOP
710         RETURN NEXT h;
711     END LOOP;
712
713     RETURN;
714 END;
715 $func$ LANGUAGE PLPGSQL;
716
717 CREATE OR REPLACE FUNCTION action.purge_circulations () RETURNS INT AS $func$
718 DECLARE
719     usr_keep_age    actor.usr_setting%ROWTYPE;
720     usr_keep_start  actor.usr_setting%ROWTYPE;
721     org_keep_age    INTERVAL;
722     org_keep_count  INT;
723
724     keep_age        INTERVAL;
725
726     target_acp      RECORD;
727     circ_chain_head action.circulation%ROWTYPE;
728     circ_chain_tail action.circulation%ROWTYPE;
729
730     purge_position  INT;
731     count_purged    INT;
732 BEGIN
733
734     count_purged := 0;
735
736     SELECT value::INTERVAL INTO org_keep_age FROM config.global_flag WHERE name = 'history.circ.retention_age' AND enabled;
737
738     SELECT value::INT INTO org_keep_count FROM config.global_flag WHERE name = 'history.circ.retention_count' AND enabled;
739     IF org_keep_count IS NULL THEN
740         RETURN count_purged; -- Gimme a count to keep, or I keep them all, forever
741     END IF;
742
743     -- First, find copies with more than keep_count non-renewal circs
744     FOR target_acp IN
745         SELECT  target_copy,
746                 COUNT(*) AS total_real_circs
747           FROM  action.circulation
748           WHERE parent_circ IS NULL
749                 AND xact_finish IS NOT NULL
750           GROUP BY target_copy
751           HAVING COUNT(*) > org_keep_count
752     LOOP
753         purge_position := 0;
754         -- And, for those, select circs that are finished and older than keep_age
755         FOR circ_chain_head IN
756             SELECT  *
757               FROM  action.circulation
758               WHERE target_copy = target_acp.target_copy
759                     AND parent_circ IS NULL
760               ORDER BY xact_start
761         LOOP
762
763             -- Stop once we've purged enough circs to hit org_keep_count
764             EXIT WHEN target_acp.total_real_circs - purge_position <= org_keep_count;
765
766             SELECT * INTO circ_chain_tail FROM action.circ_chain(circ_chain_head.id) ORDER BY xact_start DESC LIMIT 1;
767             EXIT WHEN circ_chain_tail.xact_finish IS NULL;
768
769             -- Now get the user settings, if any, to block purging if the user wants to keep more circs
770             usr_keep_age.value := NULL;
771             SELECT * INTO usr_keep_age FROM actor.usr_setting WHERE usr = circ_chain_head.usr AND name = 'history.circ.retention_age';
772
773             usr_keep_start.value := NULL;
774             SELECT * INTO usr_keep_start FROM actor.usr_setting WHERE usr = circ_chain_head.usr AND name = 'history.circ.retention_start';
775
776             IF usr_keep_age.value IS NOT NULL AND usr_keep_start.value IS NOT NULL THEN
777                 IF oils_json_to_text(usr_keep_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_keep_start.value)::TIMESTAMPTZ) THEN
778                     keep_age := AGE(NOW(), oils_json_to_text(usr_keep_start.value)::TIMESTAMPTZ);
779                 ELSE
780                     keep_age := oils_json_to_text(usr_keep_age.value)::INTERVAL;
781                 END IF;
782             ELSIF usr_keep_start.value IS NOT NULL THEN
783                 keep_age := AGE(NOW(), oils_json_to_text(usr_keep_start.value)::TIMESTAMPTZ);
784             ELSE
785                 keep_age := COALESCE( org_keep_age::INTERVAL, '2000 years'::INTERVAL );
786             END IF;
787
788             EXIT WHEN AGE(NOW(), circ_chain_tail.xact_finish) < keep_age;
789
790             -- We've passed the purging tests, purge the circ chain starting at the end
791             DELETE FROM action.circulation WHERE id = circ_chain_tail.id;
792             WHILE circ_chain_tail.parent_circ IS NOT NULL LOOP
793                 SELECT * INTO circ_chain_tail FROM action.circulation WHERE id = circ_chain_tail.parent_circ;
794                 DELETE FROM action.circulation WHERE id = circ_chain_tail.id;
795             END LOOP;
796
797             count_purged := count_purged + 1;
798             purge_position := purge_position + 1;
799
800         END LOOP;
801     END LOOP;
802 END;
803 $func$ LANGUAGE PLPGSQL;
804
805
806 CREATE OR REPLACE FUNCTION action.apply_fieldset(
807         fieldset_id IN INT,        -- id from action.fieldset
808         table_name  IN TEXT,       -- table to be updated
809         pkey_name   IN TEXT,       -- name of primary key column in that table
810         query       IN TEXT        -- query constructed by qstore (for query-based
811                                    --    fieldsets only; otherwise null
812 )
813 RETURNS TEXT AS $$
814 DECLARE
815         statement TEXT;
816         fs_status TEXT;
817         fs_pkey_value TEXT;
818         fs_query TEXT;
819         sep CHAR;
820         status_code TEXT;
821         msg TEXT;
822         update_count INT;
823         cv RECORD;
824 BEGIN
825         -- Sanity checks
826         IF fieldset_id IS NULL THEN
827                 RETURN 'Fieldset ID parameter is NULL';
828         END IF;
829         IF table_name IS NULL THEN
830                 RETURN 'Table name parameter is NULL';
831         END IF;
832         IF pkey_name IS NULL THEN
833                 RETURN 'Primary key name parameter is NULL';
834         END IF;
835         --
836         statement := 'UPDATE ' || table_name || ' SET';
837         --
838         SELECT
839                 status,
840                 quote_literal( pkey_value )
841         INTO
842                 fs_status,
843                 fs_pkey_value
844         FROM
845                 action.fieldset
846         WHERE
847                 id = fieldset_id;
848         --
849         IF fs_status IS NULL THEN
850                 RETURN 'No fieldset found for id = ' || fieldset_id;
851         ELSIF fs_status = 'APPLIED' THEN
852                 RETURN 'Fieldset ' || fieldset_id || ' has already been applied';
853         END IF;
854         --
855         sep := '';
856         FOR cv IN
857                 SELECT  col,
858                                 val
859                 FROM    action.fieldset_col_val
860                 WHERE   fieldset = fieldset_id
861         LOOP
862                 statement := statement || sep || ' ' || cv.col
863                                          || ' = ' || coalesce( quote_literal( cv.val ), 'NULL' );
864                 sep := ',';
865         END LOOP;
866         --
867         IF sep = '' THEN
868                 RETURN 'Fieldset ' || fieldset_id || ' has no column values defined';
869         END IF;
870         --
871         -- Add the WHERE clause.  This differs according to whether it's a
872         -- single-row fieldset or a query-based fieldset.
873         --
874         IF query IS NULL        AND fs_pkey_value IS NULL THEN
875                 RETURN 'Incomplete fieldset: neither a primary key nor a query available';
876         ELSIF query IS NOT NULL AND fs_pkey_value IS NULL THEN
877             fs_query := rtrim( query, ';' );
878             statement := statement || ' WHERE ' || pkey_name || ' IN ( '
879                          || fs_query || ' );';
880         ELSIF query IS NULL     AND fs_pkey_value IS NOT NULL THEN
881                 statement := statement || ' WHERE ' || pkey_name || ' = '
882                                      || fs_pkey_value || ';';
883         ELSE  -- both are not null
884                 RETURN 'Ambiguous fieldset: both a primary key and a query provided';
885         END IF;
886         --
887         -- Execute the update
888         --
889         BEGIN
890                 EXECUTE statement;
891                 GET DIAGNOSTICS update_count = ROW_COUNT;
892                 --
893                 IF UPDATE_COUNT > 0 THEN
894                         status_code := 'APPLIED';
895                         msg := NULL;
896                 ELSE
897                         status_code := 'ERROR';
898                         msg := 'No eligible rows found for fieldset ' || fieldset_id;
899         END IF;
900         EXCEPTION WHEN OTHERS THEN
901                 status_code := 'ERROR';
902                 msg := 'Unable to apply fieldset ' || fieldset_id
903                            || ': ' || sqlerrm;
904         END;
905         --
906         -- Update fieldset status
907         --
908         UPDATE action.fieldset
909         SET status       = status_code,
910             applied_time = now()
911         WHERE id = fieldset_id;
912         --
913         RETURN msg;
914 END;
915 $$ LANGUAGE plpgsql;
916
917 COMMENT ON FUNCTION action.apply_fieldset( INT, TEXT, TEXT, TEXT ) IS $$
918 /**
919  * Applies a specified fieldset, using a supplied table name and primary
920  * key name.  The query parameter should be non-null only for
921  * query-based fieldsets.
922  *
923  * Returns NULL if successful, or an error message if not.
924  */
925 $$;
926
927
928 COMMIT;