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