]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/090.schema.action.sql
Use hold current shelf lib to determine availability : opac sorting
[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 = b.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         current_shelf_lib INT REFERENCES actor.org_unit DEFERRABLE INITIALLY DEFERRED
362 );
363
364 CREATE INDEX hold_request_target_idx ON action.hold_request (target);
365 CREATE INDEX hold_request_usr_idx ON action.hold_request (usr);
366 CREATE INDEX hold_request_pickup_lib_idx ON action.hold_request (pickup_lib);
367 CREATE INDEX hold_request_current_copy_idx ON action.hold_request (current_copy);
368 CREATE INDEX hold_request_prev_check_time_idx ON action.hold_request (prev_check_time);
369 CREATE INDEX hold_request_fulfillment_staff_idx ON action.hold_request ( fulfillment_staff );
370 CREATE INDEX hold_request_requestor_idx         ON action.hold_request ( requestor );
371
372
373 CREATE TABLE action.hold_request_note (
374
375     id     BIGSERIAL PRIMARY KEY,
376     hold   BIGINT    NOT NULL REFERENCES action.hold_request (id)
377                               ON DELETE CASCADE
378                               DEFERRABLE INITIALLY DEFERRED,
379     title  TEXT      NOT NULL,
380     body   TEXT      NOT NULL,
381     slip   BOOL      NOT NULL DEFAULT FALSE,
382     pub    BOOL      NOT NULL DEFAULT FALSE,
383     staff  BOOL      NOT NULL DEFAULT FALSE  -- created by staff
384
385 );
386 CREATE INDEX ahrn_hold_idx ON action.hold_request_note (hold);
387
388
389 CREATE TABLE action.hold_notification (
390         id              SERIAL                          PRIMARY KEY,
391         hold            INT                             NOT NULL REFERENCES action.hold_request (id)
392                                                                         ON DELETE CASCADE
393                                                                         DEFERRABLE INITIALLY DEFERRED,
394         notify_staff    INT                     REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
395         notify_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
396         method          TEXT                            NOT NULL, -- email address or phone number
397         note            TEXT
398 );
399 CREATE INDEX ahn_hold_idx ON action.hold_notification (hold);
400 CREATE INDEX ahn_notify_staff_idx ON action.hold_notification ( notify_staff );
401
402 CREATE TABLE action.hold_copy_map (
403         id              BIGSERIAL       PRIMARY KEY,
404         hold            INT     NOT NULL REFERENCES action.hold_request (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
405         target_copy     BIGINT  NOT NULL, -- REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
406         CONSTRAINT copy_once_per_hold UNIQUE (hold,target_copy)
407 );
408 -- CREATE INDEX acm_hold_idx ON action.hold_copy_map (hold);
409 CREATE INDEX acm_copy_idx ON action.hold_copy_map (target_copy);
410
411 CREATE TABLE action.transit_copy (
412         id                      SERIAL                          PRIMARY KEY,
413         source_send_time        TIMESTAMP WITH TIME ZONE,
414         dest_recv_time          TIMESTAMP WITH TIME ZONE,
415         target_copy             BIGINT                          NOT NULL, -- REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance
416         source                  INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
417         dest                    INT                             NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
418         prev_hop                INT                             REFERENCES action.transit_copy (id) DEFERRABLE INITIALLY DEFERRED,
419         copy_status             INT                             NOT NULL REFERENCES config.copy_status (id) DEFERRABLE INITIALLY DEFERRED,
420         persistant_transfer     BOOL                            NOT NULL DEFAULT FALSE,
421         prev_dest       INT                             REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED
422 );
423 CREATE INDEX active_transit_dest_idx ON "action".transit_copy (dest); 
424 CREATE INDEX active_transit_source_idx ON "action".transit_copy (source);
425 CREATE INDEX active_transit_cp_idx ON "action".transit_copy (target_copy);
426
427
428 CREATE TABLE action.hold_transit_copy (
429         hold    INT     REFERENCES action.hold_request (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
430 ) INHERITS (action.transit_copy);
431 ALTER TABLE action.hold_transit_copy ADD PRIMARY KEY (id);
432 -- 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
433 CREATE INDEX active_hold_transit_dest_idx ON "action".hold_transit_copy (dest);
434 CREATE INDEX active_hold_transit_source_idx ON "action".hold_transit_copy (source);
435 CREATE INDEX active_hold_transit_cp_idx ON "action".hold_transit_copy (target_copy);
436
437
438 CREATE TABLE action.unfulfilled_hold_list (
439         id              BIGSERIAL                       PRIMARY KEY,
440         current_copy    BIGINT                          NOT NULL,
441         hold            INT                             NOT NULL,
442         circ_lib        INT                             NOT NULL,
443         fail_time       TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW()
444 );
445 CREATE INDEX uhr_hold_idx ON action.unfulfilled_hold_list (hold);
446
447 CREATE OR REPLACE VIEW action.unfulfilled_hold_loops AS
448     SELECT  u.hold,
449             c.circ_lib,
450             count(*)
451       FROM  action.unfulfilled_hold_list u
452             JOIN asset.copy c ON (c.id = u.current_copy)
453       GROUP BY 1,2;
454
455 CREATE OR REPLACE VIEW action.unfulfilled_hold_min_loop AS
456     SELECT  hold,
457             min(count)
458       FROM  action.unfulfilled_hold_loops
459       GROUP BY 1;
460
461 CREATE OR REPLACE VIEW action.unfulfilled_hold_innermost_loop AS
462     SELECT  DISTINCT l.*
463       FROM  action.unfulfilled_hold_loops l
464             JOIN action.unfulfilled_hold_min_loop m USING (hold)
465       WHERE l.count = m.min;
466
467 CREATE VIEW action.unfulfilled_hold_max_loop AS
468     SELECT  hold,
469             max(count) AS max
470       FROM  action.unfulfilled_hold_loops
471       GROUP BY 1;
472
473
474 CREATE TABLE action.fieldset (
475     id              SERIAL          PRIMARY KEY,
476     owner           INT             NOT NULL REFERENCES actor.usr (id)
477                                     DEFERRABLE INITIALLY DEFERRED,
478         owning_lib      INT             NOT NULL REFERENCES actor.org_unit (id)
479                                     DEFERRABLE INITIALLY DEFERRED,
480         status          TEXT            NOT NULL
481                                         CONSTRAINT valid_status CHECK ( status in
482                                                                         ( 'PENDING', 'APPLIED', 'ERROR' )),
483     creation_time   TIMESTAMPTZ     NOT NULL DEFAULT NOW(),
484     scheduled_time  TIMESTAMPTZ,
485     applied_time    TIMESTAMPTZ,
486     classname       TEXT            NOT NULL, -- an IDL class name
487     name            TEXT            NOT NULL,
488     stored_query    INT             REFERENCES query.stored_query (id)
489                                     DEFERRABLE INITIALLY DEFERRED,
490     pkey_value      TEXT,
491         CONSTRAINT lib_name_unique UNIQUE (owning_lib, name),
492     CONSTRAINT fieldset_one_or_the_other CHECK (
493         (stored_query IS NOT NULL AND pkey_value IS NULL) OR
494         (pkey_value IS NOT NULL AND stored_query IS NULL)
495     )
496         -- the CHECK constraint means we can update the fields for a single
497         -- row without all the extra overhead involved in a query
498 );
499
500 CREATE INDEX action_fieldset_sched_time_idx ON action.fieldset( scheduled_time );
501 CREATE INDEX action_owner_idx               ON action.fieldset( owner );
502
503
504 CREATE TABLE action.fieldset_col_val (
505     id              SERIAL  PRIMARY KEY,
506     fieldset        INT     NOT NULL REFERENCES action.fieldset
507                                          ON DELETE CASCADE
508                                          DEFERRABLE INITIALLY DEFERRED,
509     col             TEXT    NOT NULL,  -- "field" from the idl ... the column on the table
510     val             TEXT,              -- value for the column ... NULL means, well, NULL
511     CONSTRAINT fieldset_col_once_per_set UNIQUE (fieldset, col)
512 );
513
514
515 -- represents a circ chain summary
516 CREATE TYPE action.circ_chain_summary AS (
517     num_circs INTEGER,
518     start_time TIMESTAMP WITH TIME ZONE,
519     checkout_workstation TEXT,
520     last_renewal_time TIMESTAMP WITH TIME ZONE, -- NULL if no renewals
521     last_stop_fines TEXT,
522     last_stop_fines_time TIMESTAMP WITH TIME ZONE,
523     last_renewal_workstation TEXT, -- NULL if no renewals
524     last_checkin_workstation TEXT,
525     last_checkin_time TIMESTAMP WITH TIME ZONE,
526     last_checkin_scan_time TIMESTAMP WITH TIME ZONE
527 );
528
529
530 CREATE OR REPLACE FUNCTION action.circ_chain ( ctx_circ_id INTEGER ) RETURNS SETOF action.circulation AS $$
531 DECLARE
532     tmp_circ action.circulation%ROWTYPE;
533     circ_0 action.circulation%ROWTYPE;
534 BEGIN
535
536     SELECT INTO tmp_circ * FROM action.circulation WHERE id = ctx_circ_id;
537
538     IF tmp_circ IS NULL THEN
539         RETURN NEXT tmp_circ;
540     END IF;
541     circ_0 := tmp_circ;
542
543     -- find the front of the chain
544     WHILE TRUE LOOP
545         SELECT INTO tmp_circ * FROM action.circulation WHERE id = tmp_circ.parent_circ;
546         IF tmp_circ IS NULL THEN
547             EXIT;
548         END IF;
549         circ_0 := tmp_circ;
550     END LOOP;
551
552     -- now send the circs to the caller, oldest to newest
553     tmp_circ := circ_0;
554     WHILE TRUE LOOP
555         IF tmp_circ IS NULL THEN
556             EXIT;
557         END IF;
558         RETURN NEXT tmp_circ;
559         SELECT INTO tmp_circ * FROM action.circulation WHERE parent_circ = tmp_circ.id;
560     END LOOP;
561
562 END;
563 $$ LANGUAGE 'plpgsql';
564
565 CREATE OR REPLACE FUNCTION action.summarize_circ_chain ( ctx_circ_id INTEGER ) RETURNS action.circ_chain_summary AS $$
566
567 DECLARE
568
569     -- first circ in the chain
570     circ_0 action.circulation%ROWTYPE;
571
572     -- last circ in the chain
573     circ_n action.circulation%ROWTYPE;
574
575     -- circ chain under construction
576     chain action.circ_chain_summary;
577     tmp_circ action.circulation%ROWTYPE;
578
579 BEGIN
580     
581     chain.num_circs := 0;
582     FOR tmp_circ IN SELECT * FROM action.circ_chain(ctx_circ_id) LOOP
583
584         IF chain.num_circs = 0 THEN
585             circ_0 := tmp_circ;
586         END IF;
587
588         chain.num_circs := chain.num_circs + 1;
589         circ_n := tmp_circ;
590     END LOOP;
591
592     chain.start_time := circ_0.xact_start;
593     chain.last_stop_fines := circ_n.stop_fines;
594     chain.last_stop_fines_time := circ_n.stop_fines_time;
595     chain.last_checkin_time := circ_n.checkin_time;
596     chain.last_checkin_scan_time := circ_n.checkin_scan_time;
597     SELECT INTO chain.checkout_workstation name FROM actor.workstation WHERE id = circ_0.workstation;
598     SELECT INTO chain.last_checkin_workstation name FROM actor.workstation WHERE id = circ_n.checkin_workstation;
599
600     IF chain.num_circs > 1 THEN
601         chain.last_renewal_time := circ_n.xact_start;
602         SELECT INTO chain.last_renewal_workstation name FROM actor.workstation WHERE id = circ_n.workstation;
603     END IF;
604
605     RETURN chain;
606
607 END;
608 $$ LANGUAGE 'plpgsql';
609
610 -- Return the list of circ chain heads in xact_start order that the user has chosen to "retain"
611 CREATE OR REPLACE FUNCTION action.usr_visible_circs (usr_id INT) RETURNS SETOF action.circulation AS $func$
612 DECLARE
613     c               action.circulation%ROWTYPE;
614     view_age        INTERVAL;
615     usr_view_age    actor.usr_setting%ROWTYPE;
616     usr_view_start  actor.usr_setting%ROWTYPE;
617 BEGIN
618     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_age';
619     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_start';
620
621     IF usr_view_age.value IS NOT NULL AND usr_view_start.value IS NOT NULL THEN
622         -- User opted in and supplied a retention age
623         IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
624             view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
625         ELSE
626             view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
627         END IF;
628     ELSIF usr_view_start.value IS NOT NULL THEN
629         -- User opted in
630         view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
631     ELSE
632         -- User did not opt in
633         RETURN;
634     END IF;
635
636     FOR c IN
637         SELECT  *
638           FROM  action.circulation
639           WHERE usr = usr_id
640                 AND parent_circ IS NULL
641                 AND xact_start > NOW() - view_age
642           ORDER BY xact_start DESC
643     LOOP
644         RETURN NEXT c;
645     END LOOP;
646
647     RETURN;
648 END;
649 $func$ LANGUAGE PLPGSQL;
650
651 CREATE OR REPLACE FUNCTION action.usr_visible_circ_copies( INTEGER ) RETURNS SETOF BIGINT AS $$
652     SELECT DISTINCT(target_copy) FROM action.usr_visible_circs($1)
653 $$ LANGUAGE SQL;
654
655 CREATE OR REPLACE FUNCTION action.usr_visible_holds (usr_id INT) RETURNS SETOF action.hold_request AS $func$
656 DECLARE
657     h               action.hold_request%ROWTYPE;
658     view_age        INTERVAL;
659     view_count      INT;
660     usr_view_count  actor.usr_setting%ROWTYPE;
661     usr_view_age    actor.usr_setting%ROWTYPE;
662     usr_view_start  actor.usr_setting%ROWTYPE;
663 BEGIN
664     SELECT * INTO usr_view_count FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_count';
665     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_age';
666     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_start';
667
668     FOR h IN
669         SELECT  *
670           FROM  action.hold_request
671           WHERE usr = usr_id
672                 AND fulfillment_time IS NULL
673                 AND cancel_time IS NULL
674           ORDER BY request_time DESC
675     LOOP
676         RETURN NEXT h;
677     END LOOP;
678
679     IF usr_view_start.value IS NULL THEN
680         RETURN;
681     END IF;
682
683     IF usr_view_age.value IS NOT NULL THEN
684         -- User opted in and supplied a retention age
685         IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
686             view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
687         ELSE
688             view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
689         END IF;
690     ELSE
691         -- User opted in
692         view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
693     END IF;
694
695     IF usr_view_count.value IS NOT NULL THEN
696         view_count := oils_json_to_text(usr_view_count.value)::INT;
697     ELSE
698         view_count := 1000;
699     END IF;
700
701     -- show some fulfilled/canceled holds
702     FOR h IN
703         SELECT  *
704           FROM  action.hold_request
705           WHERE usr = usr_id
706                 AND ( fulfillment_time IS NOT NULL OR cancel_time IS NOT NULL )
707                 AND request_time > NOW() - view_age
708           ORDER BY request_time DESC
709           LIMIT view_count
710     LOOP
711         RETURN NEXT h;
712     END LOOP;
713
714     RETURN;
715 END;
716 $func$ LANGUAGE PLPGSQL;
717
718 CREATE OR REPLACE FUNCTION action.purge_circulations () RETURNS INT AS $func$
719 DECLARE
720     usr_keep_age    actor.usr_setting%ROWTYPE;
721     usr_keep_start  actor.usr_setting%ROWTYPE;
722     org_keep_age    INTERVAL;
723     org_keep_count  INT;
724
725     keep_age        INTERVAL;
726
727     target_acp      RECORD;
728     circ_chain_head action.circulation%ROWTYPE;
729     circ_chain_tail action.circulation%ROWTYPE;
730
731     purge_position  INT;
732     count_purged    INT;
733 BEGIN
734
735     count_purged := 0;
736
737     SELECT value::INTERVAL INTO org_keep_age FROM config.global_flag WHERE name = 'history.circ.retention_age' AND enabled;
738
739     SELECT value::INT INTO org_keep_count FROM config.global_flag WHERE name = 'history.circ.retention_count' AND enabled;
740     IF org_keep_count IS NULL THEN
741         RETURN count_purged; -- Gimme a count to keep, or I keep them all, forever
742     END IF;
743
744     -- First, find copies with more than keep_count non-renewal circs
745     FOR target_acp IN
746         SELECT  target_copy,
747                 COUNT(*) AS total_real_circs
748           FROM  action.circulation
749           WHERE parent_circ IS NULL
750                 AND xact_finish IS NOT NULL
751           GROUP BY target_copy
752           HAVING COUNT(*) > org_keep_count
753     LOOP
754         purge_position := 0;
755         -- And, for those, select circs that are finished and older than keep_age
756         FOR circ_chain_head IN
757             SELECT  *
758               FROM  action.circulation
759               WHERE target_copy = target_acp.target_copy
760                     AND parent_circ IS NULL
761               ORDER BY xact_start
762         LOOP
763
764             -- Stop once we've purged enough circs to hit org_keep_count
765             EXIT WHEN target_acp.total_real_circs - purge_position <= org_keep_count;
766
767             SELECT * INTO circ_chain_tail FROM action.circ_chain(circ_chain_head.id) ORDER BY xact_start DESC LIMIT 1;
768             EXIT WHEN circ_chain_tail.xact_finish IS NULL;
769
770             -- Now get the user settings, if any, to block purging if the user wants to keep more circs
771             usr_keep_age.value := NULL;
772             SELECT * INTO usr_keep_age FROM actor.usr_setting WHERE usr = circ_chain_head.usr AND name = 'history.circ.retention_age';
773
774             usr_keep_start.value := NULL;
775             SELECT * INTO usr_keep_start FROM actor.usr_setting WHERE usr = circ_chain_head.usr AND name = 'history.circ.retention_start';
776
777             IF usr_keep_age.value IS NOT NULL AND usr_keep_start.value IS NOT NULL THEN
778                 IF oils_json_to_text(usr_keep_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_keep_start.value)::TIMESTAMPTZ) THEN
779                     keep_age := AGE(NOW(), oils_json_to_text(usr_keep_start.value)::TIMESTAMPTZ);
780                 ELSE
781                     keep_age := oils_json_to_text(usr_keep_age.value)::INTERVAL;
782                 END IF;
783             ELSIF usr_keep_start.value IS NOT NULL THEN
784                 keep_age := AGE(NOW(), oils_json_to_text(usr_keep_start.value)::TIMESTAMPTZ);
785             ELSE
786                 keep_age := COALESCE( org_keep_age::INTERVAL, '2000 years'::INTERVAL );
787             END IF;
788
789             EXIT WHEN AGE(NOW(), circ_chain_tail.xact_finish) < keep_age;
790
791             -- We've passed the purging tests, purge the circ chain starting at the end
792             DELETE FROM action.circulation WHERE id = circ_chain_tail.id;
793             WHILE circ_chain_tail.parent_circ IS NOT NULL LOOP
794                 SELECT * INTO circ_chain_tail FROM action.circulation WHERE id = circ_chain_tail.parent_circ;
795                 DELETE FROM action.circulation WHERE id = circ_chain_tail.id;
796             END LOOP;
797
798             count_purged := count_purged + 1;
799             purge_position := purge_position + 1;
800
801         END LOOP;
802     END LOOP;
803 END;
804 $func$ LANGUAGE PLPGSQL;
805
806
807 CREATE OR REPLACE FUNCTION action.apply_fieldset(
808         fieldset_id IN INT,        -- id from action.fieldset
809         table_name  IN TEXT,       -- table to be updated
810         pkey_name   IN TEXT,       -- name of primary key column in that table
811         query       IN TEXT        -- query constructed by qstore (for query-based
812                                    --    fieldsets only; otherwise null
813 )
814 RETURNS TEXT AS $$
815 DECLARE
816         statement TEXT;
817         fs_status TEXT;
818         fs_pkey_value TEXT;
819         fs_query TEXT;
820         sep CHAR;
821         status_code TEXT;
822         msg TEXT;
823         update_count INT;
824         cv RECORD;
825 BEGIN
826         -- Sanity checks
827         IF fieldset_id IS NULL THEN
828                 RETURN 'Fieldset ID parameter is NULL';
829         END IF;
830         IF table_name IS NULL THEN
831                 RETURN 'Table name parameter is NULL';
832         END IF;
833         IF pkey_name IS NULL THEN
834                 RETURN 'Primary key name parameter is NULL';
835         END IF;
836         --
837         statement := 'UPDATE ' || table_name || ' SET';
838         --
839         SELECT
840                 status,
841                 quote_literal( pkey_value )
842         INTO
843                 fs_status,
844                 fs_pkey_value
845         FROM
846                 action.fieldset
847         WHERE
848                 id = fieldset_id;
849         --
850         IF fs_status IS NULL THEN
851                 RETURN 'No fieldset found for id = ' || fieldset_id;
852         ELSIF fs_status = 'APPLIED' THEN
853                 RETURN 'Fieldset ' || fieldset_id || ' has already been applied';
854         END IF;
855         --
856         sep := '';
857         FOR cv IN
858                 SELECT  col,
859                                 val
860                 FROM    action.fieldset_col_val
861                 WHERE   fieldset = fieldset_id
862         LOOP
863                 statement := statement || sep || ' ' || cv.col
864                                          || ' = ' || coalesce( quote_literal( cv.val ), 'NULL' );
865                 sep := ',';
866         END LOOP;
867         --
868         IF sep = '' THEN
869                 RETURN 'Fieldset ' || fieldset_id || ' has no column values defined';
870         END IF;
871         --
872         -- Add the WHERE clause.  This differs according to whether it's a
873         -- single-row fieldset or a query-based fieldset.
874         --
875         IF query IS NULL        AND fs_pkey_value IS NULL THEN
876                 RETURN 'Incomplete fieldset: neither a primary key nor a query available';
877         ELSIF query IS NOT NULL AND fs_pkey_value IS NULL THEN
878             fs_query := rtrim( query, ';' );
879             statement := statement || ' WHERE ' || pkey_name || ' IN ( '
880                          || fs_query || ' );';
881         ELSIF query IS NULL     AND fs_pkey_value IS NOT NULL THEN
882                 statement := statement || ' WHERE ' || pkey_name || ' = '
883                                      || fs_pkey_value || ';';
884         ELSE  -- both are not null
885                 RETURN 'Ambiguous fieldset: both a primary key and a query provided';
886         END IF;
887         --
888         -- Execute the update
889         --
890         BEGIN
891                 EXECUTE statement;
892                 GET DIAGNOSTICS update_count = ROW_COUNT;
893                 --
894                 IF UPDATE_COUNT > 0 THEN
895                         status_code := 'APPLIED';
896                         msg := NULL;
897                 ELSE
898                         status_code := 'ERROR';
899                         msg := 'No eligible rows found for fieldset ' || fieldset_id;
900         END IF;
901         EXCEPTION WHEN OTHERS THEN
902                 status_code := 'ERROR';
903                 msg := 'Unable to apply fieldset ' || fieldset_id
904                            || ': ' || sqlerrm;
905         END;
906         --
907         -- Update fieldset status
908         --
909         UPDATE action.fieldset
910         SET status       = status_code,
911             applied_time = now()
912         WHERE id = fieldset_id;
913         --
914         RETURN msg;
915 END;
916 $$ LANGUAGE plpgsql;
917
918 COMMENT ON FUNCTION action.apply_fieldset( INT, TEXT, TEXT, TEXT ) IS $$
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 COMMIT;