]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/version-upgrade/2.0-2.1-upgrade-db.sql
e245c831c037952d59bda0334eee3c2ba9e6ef1f
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.0-2.1-upgrade-db.sql
1 -- 0498
2 -- Rather than polluting the public schema with general Evergreen
3 -- functions, carve out a dedicated schema.  It might already exist,
4 -- so do it before the transaction starts
5 CREATE SCHEMA evergreen;
6
7 BEGIN;
8
9 -- 0425
10 ALTER TABLE permission.grp_tree
11         ADD COLUMN hold_priority INT NOT NULL DEFAULT 0;
12
13 -- 0430 and friends
14 ALTER TABLE config.hold_matrix_matchpoint
15     ADD COLUMN strict_ou_match BOOL NOT NULL DEFAULT FALSE,
16     ADD COLUMN marc_bib_level text,
17     DROP CONSTRAINT hous_once_per_grp_loc_mod_marc,
18     DROP CONSTRAINT hold_matrix_matchpoint_marc_form_fkey,
19     DROP CONSTRAINT hold_matrix_matchpoint_marc_type_fkey,
20     DROP CONSTRAINT hold_matrix_matchpoint_marc_vr_format_fkey;
21
22
23
24 -- Replace all uses of PostgreSQL's built-in LOWER() function with
25 -- a more locale-savvy PLPERLU evergreen.lowercase() function
26 CREATE OR REPLACE FUNCTION evergreen.lowercase( TEXT ) RETURNS TEXT AS $$
27     return lc(shift);
28 $$ LANGUAGE PLPERLU STRICT IMMUTABLE;
29
30 -- 0500
31 CREATE OR REPLACE FUNCTION evergreen.change_db_setting(setting_name TEXT, settings TEXT[]) RETURNS VOID AS $$
32 BEGIN
33 EXECUTE 'ALTER DATABASE ' || quote_ident(current_database()) || ' SET ' || quote_ident(setting_name) || ' = ' || array_to_string(settings, ',');
34 END;
35
36 $$ LANGUAGE plpgsql;
37
38 -- 0501
39 SELECT evergreen.change_db_setting('search_path', ARRAY['evergreen','public','pg_catalog']);
40
41 -- Fix function breakage due to short search path
42 CREATE OR REPLACE FUNCTION evergreen.force_unicode_normal_form(string TEXT, form TEXT) RETURNS TEXT AS $func$
43 use Unicode::Normalize 'normalize';
44 return normalize($_[1],$_[0]); # reverse the params
45 $func$ LANGUAGE PLPERLU;
46
47 CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
48 BEGIN
49     NEW.value := evergreen.force_unicode_normal_form(NEW.value,'NFC');
50     RETURN NEW;
51 END;
52 $$ LANGUAGE PLPGSQL;
53
54 CREATE OR REPLACE FUNCTION evergreen.xml_escape(str TEXT) RETURNS text AS $$
55     SELECT REPLACE(REPLACE(REPLACE($1,
56        '&', '&'),
57        '<', '&lt;'),
58        '>', '&gt;');
59 $$ LANGUAGE SQL IMMUTABLE;
60
61 CREATE OR REPLACE FUNCTION evergreen.maintain_901 () RETURNS TRIGGER AS $func$
62 DECLARE
63     use_id_for_tcn BOOLEAN;
64 BEGIN
65     -- Remove any existing 901 fields before we insert the authoritative one
66     NEW.marc := REGEXP_REPLACE(NEW.marc, E'<datafield[^>]*?tag="901".+?</datafield>', '', 'g');
67
68     IF TG_TABLE_SCHEMA = 'biblio' THEN
69         -- Set TCN value to record ID?
70         SELECT enabled FROM config.global_flag INTO use_id_for_tcn
71             WHERE name = 'cat.bib.use_id_for_tcn';
72
73         IF use_id_for_tcn = 't' THEN
74             NEW.tcn_value := NEW.id;
75         END IF;
76
77         NEW.marc := REGEXP_REPLACE(
78             NEW.marc,
79             E'(</(?:[^:]*?:)?record>)',
80             E'<datafield tag="901" ind1=" " ind2=" ">' ||
81                 '<subfield code="a">' || evergreen.xml_escape(NEW.tcn_value) || E'</subfield>' ||
82                 '<subfield code="b">' || evergreen.xml_escape(NEW.tcn_source) || E'</subfield>' ||
83                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
84                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
85                 CASE WHEN NEW.owner IS NOT NULL THEN '<subfield code="o">' || NEW.owner || E'</subfield>' ELSE '' END ||
86                 CASE WHEN NEW.share_depth IS NOT NULL THEN '<subfield code="d">' || NEW.share_depth || E'</subfield>' ELSE '' END ||
87              E'</datafield>\\1'
88         );
89     ELSIF TG_TABLE_SCHEMA = 'authority' THEN
90         NEW.marc := REGEXP_REPLACE(
91             NEW.marc,
92             E'(</(?:[^:]*?:)?record>)',
93             E'<datafield tag="901" ind1=" " ind2=" ">' ||
94                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
95                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
96              E'</datafield>\\1'
97         );
98     ELSIF TG_TABLE_SCHEMA = 'serial' THEN
99         NEW.marc := REGEXP_REPLACE(
100             NEW.marc,
101             E'(</(?:[^:]*?:)?record>)',
102             E'<datafield tag="901" ind1=" " ind2=" ">' ||
103                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
104                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
105                 '<subfield code="o">' || NEW.owning_lib || E'</subfield>' ||
106                 CASE WHEN NEW.record IS NOT NULL THEN '<subfield code="r">' || NEW.record || E'</subfield>' ELSE '' END ||
107              E'</datafield>\\1'
108         );
109     ELSE
110         NEW.marc := REGEXP_REPLACE(
111             NEW.marc,
112             E'(</(?:[^:]*?:)?record>)',
113             E'<datafield tag="901" ind1=" " ind2=" ">' ||
114                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
115                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
116              E'</datafield>\\1'
117         );
118     END IF;
119
120     RETURN NEW;
121 END;
122 $func$ LANGUAGE PLPGSQL;
123
124 CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT) RETURNS anyarray AS $$ SELECT ARRAY_ACCUM(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2; $$ LANGUAGE SQL;
125
126 CREATE OR REPLACE FUNCTION evergreen.lpad_number_substrings( TEXT, TEXT, INT ) RETURNS TEXT AS $$
127     my $string = shift;
128     my $pad = shift;
129     my $len = shift;
130     my $find = $len - 1;
131
132     while ($string =~ /(?:^|\D)(\d{1,$find})(?:$|\D)/) {
133         my $padded = $1;
134         $padded = $pad x ($len - length($padded)) . $padded;
135         $string =~ s/$1/$padded/sg;
136     }
137
138     return $string;
139 $$ LANGUAGE PLPERLU;
140
141 -- 0477
142 ALTER TABLE config.hard_due_date DROP CONSTRAINT hard_due_date_name_check;
143
144 -- 0478
145 CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
146
147     use strict;
148     use Unicode::Normalize;
149     use Encode;
150
151     my $str = decode_utf8(shift);
152     my $sf = shift;
153
154     # Apply NACO normalization to input string; based on
155     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
156     #
157     # Note that unlike a strict reading of the NACO normalization rules,
158     # output is returned as lowercase instead of uppercase for compatibility
159     # with previous versions of the Evergreen naco_normalize routine.
160
161     # Convert to upper-case first; even though final output will be lowercase, doing this will
162     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
163     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
164     $str = uc $str;
165
166     # remove non-filing strings
167     $str =~ s/\x{0098}.*?\x{009C}//g;
168
169     $str = NFKD($str);
170
171     # additional substitutions - 3.6.
172     $str =~ s/\x{00C6}/AE/g;
173     $str =~ s/\x{00DE}/TH/g;
174     $str =~ s/\x{0152}/OE/g;
175     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
176
177     # transformations based on Unicode category codes
178     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
179
180         if ($sf && $sf =~ /^a/o) {
181                 my $commapos = index($str, ',');
182                 if ($commapos > -1) {
183                         if ($commapos != length($str) - 1) {
184                 $str =~ s/,/\x07/; # preserve first comma
185                         }
186                 }
187         }
188
189     # since we've stripped out the control characters, we can now
190     # use a few as placeholders temporarily
191     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
192     $str =~ s/[\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\p{Sk}\p{Sm}\p{So}\p{Zl}\p{Zp}\p{Zs}]/ /g;
193     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
194
195     # decimal digits
196     $str =~ tr/\x{0660}-\x{0669}\x{06F0}-\x{06F9}\x{07C0}-\x{07C9}\x{0966}-\x{096F}\x{09E6}-\x{09EF}\x{0A66}-\x{0A6F}\x{0AE6}-\x{0AEF}\x{0B66}-\x{0B6F}\x{0BE6}-\x{0BEF}\x{0C66}-\x{0C6F}\x{0CE6}-\x{0CEF}\x{0D66}-\x{0D6F}\x{0E50}-\x{0E59}\x{0ED0}-\x{0ED9}\x{0F20}-\x{0F29}\x{1040}-\x{1049}\x{1090}-\x{1099}\x{17E0}-\x{17E9}\x{1810}-\x{1819}\x{1946}-\x{194F}\x{19D0}-\x{19D9}\x{1A80}-\x{1A89}\x{1A90}-\x{1A99}\x{1B50}-\x{1B59}\x{1BB0}-\x{1BB9}\x{1C40}-\x{1C49}\x{1C50}-\x{1C59}\x{A620}-\x{A629}\x{A8D0}-\x{A8D9}\x{A900}-\x{A909}\x{A9D0}-\x{A9D9}\x{AA50}-\x{AA59}\x{ABF0}-\x{ABF9}\x{FF10}-\x{FF19}/0-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-9/;
197
198     # intentionally skipping step 8 of the NACO algorithm; if the string
199     # gets normalized away, that's fine.
200
201     # leading and trailing spaces
202     $str =~ s/\s+/ /g;
203     $str =~ s/^\s+//;
204     $str =~ s/\s+$//g;
205
206     return lc $str;
207 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
208
209 -- 0479
210 CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
211     WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
212             SELECT $1, 0
213         UNION
214             SELECT pgt.parent, gad.distance+1
215             FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON pgt.id = gad.id
216             WHERE pgt.parent IS NOT NULL
217     )
218     SELECT * FROM grp_ancestors_distance;
219 $$ LANGUAGE SQL STABLE;
220
221 CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
222     WITH RECURSIVE grp_descendants_distance(id, distance) AS (
223             SELECT $1, 0
224         UNION
225             SELECT pgt.id, gdd.distance+1
226             FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON pgt.parent = gdd.id
227     )
228     SELECT * FROM grp_descendants_distance;
229 $$ LANGUAGE SQL STABLE;
230
231 CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
232     WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
233             SELECT $1, 0
234         UNION
235             SELECT ou.parent_ou, ouad.distance+1
236             FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON ou.id = ouad.id
237             WHERE ou.parent_ou IS NOT NULL
238     )
239     SELECT * FROM org_unit_ancestors_distance;
240 $$ LANGUAGE SQL STABLE;
241
242 CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
243     WITH RECURSIVE org_unit_descendants_distance(id, distance) AS (
244             SELECT $1, 0
245         UNION
246             SELECT ou.id, oudd.distance+1
247             FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON ou.parent_ou = oudd.id
248     )
249     SELECT * FROM org_unit_descendants_distance;
250 $$ LANGUAGE SQL STABLE;
251
252 CREATE TABLE config.circ_matrix_weights (
253     id                      SERIAL  PRIMARY KEY,
254     name                    TEXT    NOT NULL UNIQUE,
255     org_unit                NUMERIC(6,2)   NOT NULL,
256     grp                     NUMERIC(6,2)   NOT NULL,
257     circ_modifier           NUMERIC(6,2)   NOT NULL,
258     marc_type               NUMERIC(6,2)   NOT NULL,
259     marc_form               NUMERIC(6,2)   NOT NULL,
260     marc_vr_format          NUMERIC(6,2)   NOT NULL,
261     copy_circ_lib           NUMERIC(6,2)   NOT NULL,
262     copy_owning_lib         NUMERIC(6,2)   NOT NULL,
263     user_home_ou            NUMERIC(6,2)   NOT NULL,
264     ref_flag                NUMERIC(6,2)   NOT NULL,
265     juvenile_flag           NUMERIC(6,2)   NOT NULL,
266     is_renewal              NUMERIC(6,2)   NOT NULL,
267     usr_age_lower_bound     NUMERIC(6,2)   NOT NULL,
268     usr_age_upper_bound     NUMERIC(6,2)   NOT NULL
269 );
270
271 CREATE TABLE config.hold_matrix_weights (
272     id                      SERIAL  PRIMARY KEY,
273     name                    TEXT    NOT NULL UNIQUE,
274     user_home_ou            NUMERIC(6,2)   NOT NULL,
275     request_ou              NUMERIC(6,2)   NOT NULL,
276     pickup_ou               NUMERIC(6,2)   NOT NULL,
277     item_owning_ou          NUMERIC(6,2)   NOT NULL,
278     item_circ_ou            NUMERIC(6,2)   NOT NULL,
279     usr_grp                 NUMERIC(6,2)   NOT NULL,
280     requestor_grp           NUMERIC(6,2)   NOT NULL,
281     circ_modifier           NUMERIC(6,2)   NOT NULL,
282     marc_type               NUMERIC(6,2)   NOT NULL,
283     marc_form               NUMERIC(6,2)   NOT NULL,
284     marc_vr_format          NUMERIC(6,2)   NOT NULL,
285     juvenile_flag           NUMERIC(6,2)   NOT NULL,
286     ref_flag                NUMERIC(6,2)   NOT NULL
287 );
288
289 CREATE TABLE config.weight_assoc (
290     id                      SERIAL  PRIMARY KEY,
291     active                  BOOL    NOT NULL,
292     org_unit                INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
293     circ_weights            INT     REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
294     hold_weights            INT     REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
295 );
296 CREATE UNIQUE INDEX cwa_one_active_per_ou ON config.weight_assoc (org_unit) WHERE active;
297
298 INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound) VALUES 
299     ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
300     ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
301     ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
302     ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
303
304 INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag) VALUES
305     ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
306     ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
307     ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
308     ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
309
310 INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES
311     (true, 1, 1, 1);
312
313 -- 0480
314 CREATE OR REPLACE FUNCTION actor.usr_purge_data(
315         src_usr  IN INTEGER,
316         specified_dest_usr IN INTEGER
317 ) RETURNS VOID AS $$
318 DECLARE
319         suffix TEXT;
320         renamable_row RECORD;
321         dest_usr INTEGER;
322 BEGIN
323
324         IF specified_dest_usr IS NULL THEN
325                 dest_usr := 1; -- Admin user on stock installs
326         ELSE
327                 dest_usr := specified_dest_usr;
328         END IF;
329
330         UPDATE actor.usr SET
331                 active = FALSE,
332                 card = NULL,
333                 mailing_address = NULL,
334                 billing_address = NULL
335         WHERE id = src_usr;
336
337         -- acq.*
338         UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
339         UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
340         UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
341         UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
342         UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
343         UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
344         DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
345
346         -- Update with a rename to avoid collisions
347         FOR renamable_row in
348                 SELECT id, name
349                 FROM   acq.picklist
350                 WHERE  owner = src_usr
351         LOOP
352                 suffix := ' (' || src_usr || ')';
353                 LOOP
354                         BEGIN
355                                 UPDATE  acq.picklist
356                                 SET     owner = dest_usr, name = name || suffix
357                                 WHERE   id = renamable_row.id;
358                         EXCEPTION WHEN unique_violation THEN
359                                 suffix := suffix || ' ';
360                                 CONTINUE;
361                         END;
362                         EXIT;
363                 END LOOP;
364         END LOOP;
365
366         UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
367         UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
368         UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
369         UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
370         UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
371         UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
372         UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
373         UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
374
375         -- action.*
376         DELETE FROM action.circulation WHERE usr = src_usr;
377         UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
378         UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
379         UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
380         UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
381         UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
382         DELETE FROM action.hold_request WHERE usr = src_usr;
383         UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
384         UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
385         DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
386         UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
387         DELETE FROM action.survey_response WHERE usr = src_usr;
388         UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
389
390         -- actor.*
391         DELETE FROM actor.card WHERE usr = src_usr;
392         DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
393
394         -- The following update is intended to avoid transient violations of a foreign
395         -- key constraint, whereby actor.usr_address references itself.  It may not be
396         -- necessary, but it does no harm.
397         UPDATE actor.usr_address SET replaces = NULL
398                 WHERE usr = src_usr AND replaces IS NOT NULL;
399         DELETE FROM actor.usr_address WHERE usr = src_usr;
400         DELETE FROM actor.usr_note WHERE usr = src_usr;
401         UPDATE actor.usr_note SET creator = dest_usr WHERE creator = src_usr;
402         DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
403         UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
404         DELETE FROM actor.usr_setting WHERE usr = src_usr;
405         DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
406         UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
407
408         -- asset.*
409         UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
410         UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
411         UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
412         UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
413         UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
414         UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
415
416         -- auditor.*
417         DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
418         DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
419         UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
420         UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
421         UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
422         UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
423         UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
424         UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
425
426         -- biblio.*
427         UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
428         UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
429         UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
430         UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
431
432         -- container.*
433         -- Update buckets with a rename to avoid collisions
434         FOR renamable_row in
435                 SELECT id, name
436                 FROM   container.biblio_record_entry_bucket
437                 WHERE  owner = src_usr
438         LOOP
439                 suffix := ' (' || src_usr || ')';
440                 LOOP
441                         BEGIN
442                                 UPDATE  container.biblio_record_entry_bucket
443                                 SET     owner = dest_usr, name = name || suffix
444                                 WHERE   id = renamable_row.id;
445                         EXCEPTION WHEN unique_violation THEN
446                                 suffix := suffix || ' ';
447                                 CONTINUE;
448                         END;
449                         EXIT;
450                 END LOOP;
451         END LOOP;
452
453         FOR renamable_row in
454                 SELECT id, name
455                 FROM   container.call_number_bucket
456                 WHERE  owner = src_usr
457         LOOP
458                 suffix := ' (' || src_usr || ')';
459                 LOOP
460                         BEGIN
461                                 UPDATE  container.call_number_bucket
462                                 SET     owner = dest_usr, name = name || suffix
463                                 WHERE   id = renamable_row.id;
464                         EXCEPTION WHEN unique_violation THEN
465                                 suffix := suffix || ' ';
466                                 CONTINUE;
467                         END;
468                         EXIT;
469                 END LOOP;
470         END LOOP;
471
472         FOR renamable_row in
473                 SELECT id, name
474                 FROM   container.copy_bucket
475                 WHERE  owner = src_usr
476         LOOP
477                 suffix := ' (' || src_usr || ')';
478                 LOOP
479                         BEGIN
480                                 UPDATE  container.copy_bucket
481                                 SET     owner = dest_usr, name = name || suffix
482                                 WHERE   id = renamable_row.id;
483                         EXCEPTION WHEN unique_violation THEN
484                                 suffix := suffix || ' ';
485                                 CONTINUE;
486                         END;
487                         EXIT;
488                 END LOOP;
489         END LOOP;
490
491         FOR renamable_row in
492                 SELECT id, name
493                 FROM   container.user_bucket
494                 WHERE  owner = src_usr
495         LOOP
496                 suffix := ' (' || src_usr || ')';
497                 LOOP
498                         BEGIN
499                                 UPDATE  container.user_bucket
500                                 SET     owner = dest_usr, name = name || suffix
501                                 WHERE   id = renamable_row.id;
502                         EXCEPTION WHEN unique_violation THEN
503                                 suffix := suffix || ' ';
504                                 CONTINUE;
505                         END;
506                         EXIT;
507                 END LOOP;
508         END LOOP;
509
510         DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
511
512         -- money.*
513         DELETE FROM money.billable_xact WHERE usr = src_usr;
514         DELETE FROM money.collections_tracker WHERE usr = src_usr;
515         UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
516
517         -- permission.*
518         DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
519         DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
520         DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
521         DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
522
523         -- reporter.*
524         -- Update with a rename to avoid collisions
525         BEGIN
526                 FOR renamable_row in
527                         SELECT id, name
528                         FROM   reporter.output_folder
529                         WHERE  owner = src_usr
530                 LOOP
531                         suffix := ' (' || src_usr || ')';
532                         LOOP
533                                 BEGIN
534                                         UPDATE  reporter.output_folder
535                                         SET     owner = dest_usr, name = name || suffix
536                                         WHERE   id = renamable_row.id;
537                                 EXCEPTION WHEN unique_violation THEN
538                                         suffix := suffix || ' ';
539                                         CONTINUE;
540                                 END;
541                                 EXIT;
542                         END LOOP;
543                 END LOOP;
544         EXCEPTION WHEN undefined_table THEN
545                 -- do nothing
546         END;
547
548         BEGIN
549                 UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
550         EXCEPTION WHEN undefined_table THEN
551                 -- do nothing
552         END;
553
554         -- Update with a rename to avoid collisions
555         BEGIN
556                 FOR renamable_row in
557                         SELECT id, name
558                         FROM   reporter.report_folder
559                         WHERE  owner = src_usr
560                 LOOP
561                         suffix := ' (' || src_usr || ')';
562                         LOOP
563                                 BEGIN
564                                         UPDATE  reporter.report_folder
565                                         SET     owner = dest_usr, name = name || suffix
566                                         WHERE   id = renamable_row.id;
567                                 EXCEPTION WHEN unique_violation THEN
568                                         suffix := suffix || ' ';
569                                         CONTINUE;
570                                 END;
571                                 EXIT;
572                         END LOOP;
573                 END LOOP;
574         EXCEPTION WHEN undefined_table THEN
575                 -- do nothing
576         END;
577
578         BEGIN
579                 UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
580         EXCEPTION WHEN undefined_table THEN
581                 -- do nothing
582         END;
583
584         BEGIN
585                 UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
586         EXCEPTION WHEN undefined_table THEN
587                 -- do nothing
588         END;
589
590         -- Update with a rename to avoid collisions
591         BEGIN
592                 FOR renamable_row in
593                         SELECT id, name
594                         FROM   reporter.template_folder
595                         WHERE  owner = src_usr
596                 LOOP
597                         suffix := ' (' || src_usr || ')';
598                         LOOP
599                                 BEGIN
600                                         UPDATE  reporter.template_folder
601                                         SET     owner = dest_usr, name = name || suffix
602                                         WHERE   id = renamable_row.id;
603                                 EXCEPTION WHEN unique_violation THEN
604                                         suffix := suffix || ' ';
605                                         CONTINUE;
606                                 END;
607                                 EXIT;
608                         END LOOP;
609                 END LOOP;
610         EXCEPTION WHEN undefined_table THEN
611         -- do nothing
612         END;
613
614         -- vandelay.*
615         -- Update with a rename to avoid collisions
616         FOR renamable_row in
617                 SELECT id, name
618                 FROM   vandelay.queue
619                 WHERE  owner = src_usr
620         LOOP
621                 suffix := ' (' || src_usr || ')';
622                 LOOP
623                         BEGIN
624                                 UPDATE  vandelay.queue
625                                 SET     owner = dest_usr, name = name || suffix
626                                 WHERE   id = renamable_row.id;
627                         EXCEPTION WHEN unique_violation THEN
628                                 suffix := suffix || ' ';
629                                 CONTINUE;
630                         END;
631                         EXIT;
632                 END LOOP;
633         END LOOP;
634
635 END;
636 $$ LANGUAGE plpgsql;
637
638 -- 0482, 0487, and parts of others
639 -- Circ matchpoint table changes
640 ALTER TABLE config.circ_matrix_matchpoint
641     ALTER COLUMN circulate DROP NOT NULL, -- Fallthrough enable
642     ALTER COLUMN circulate DROP DEFAULT, -- Stop defaulting to true to enable default to fallthrough
643     ALTER COLUMN duration_rule DROP NOT NULL, -- Fallthrough enable
644     ALTER COLUMN recurring_fine_rule DROP NOT NULL, -- Fallthrough enable
645     ALTER COLUMN max_fine_rule DROP NOT NULL, -- Fallthrough enable
646     ADD COLUMN renewals INT, -- Renewals override
647     ADD COLUMN user_home_ou INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
648     ADD COLUMN grace_period INTERVAL,
649     ADD COLUMN marc_bib_level text,
650     DROP CONSTRAINT ep_once_per_grp_loc_mod_marc,
651     DROP CONSTRAINT circ_matrix_matchpoint_marc_form_fkey,
652     DROP CONSTRAINT circ_matrix_matchpoint_marc_type_fkey,
653     DROP CONSTRAINT circ_matrix_matchpoint_marc_vr_format_fkey;
654
655 -- Clean up tables before making normalized index
656
657 CREATE OR REPLACE FUNCTION action.cleanup_matrix_matchpoints() RETURNS void AS $func$
658 DECLARE
659     temp_row    RECORD;
660 BEGIN
661     -- Circ Matrix
662     FOR temp_row IN
663         SELECT org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_lower_bound, usr_age_upper_bound, COUNT(id) as rowcount, MIN(id) as firstrow
664         FROM config.circ_matrix_matchpoint
665         WHERE active
666         GROUP BY org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_lower_bound, usr_age_upper_bound
667         HAVING COUNT(id) > 1 LOOP
668
669         UPDATE config.circ_matrix_matchpoint SET active=false
670             WHERE id > temp_row.firstrow
671                 AND org_unit = temp_row.org_unit
672                 AND grp = temp_row.grp
673                 AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
674                 AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
675                 AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
676                 AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
677                 AND copy_circ_lib       IS NOT DISTINCT FROM temp_row.copy_circ_lib
678                 AND copy_owning_lib     IS NOT DISTINCT FROM temp_row.copy_owning_lib
679                 AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
680                 AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag
681                 AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
682                 AND is_renewal          IS NOT DISTINCT FROM temp_row.is_renewal
683                 AND usr_age_lower_bound IS NOT DISTINCT FROM temp_row.usr_age_lower_bound
684                 AND usr_age_upper_bound IS NOT DISTINCT FROM temp_row.usr_age_upper_bound;
685     END LOOP;
686
687     -- Hold Matrix
688     FOR temp_row IN
689         SELECT user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag, COUNT(id) as rowcount, MIN(id) as firstrow
690         FROM config.hold_matrix_matchpoint
691         WHERE active
692         GROUP BY user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag
693         HAVING COUNT(id) > 1 LOOP
694
695         UPDATE config.hold_matrix_matchpoint SET active=false
696             WHERE id > temp_row.firstrow
697                 AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
698                 AND request_ou          IS NOT DISTINCT FROM temp_row.request_ou
699                 AND pickup_ou           IS NOT DISTINCT FROM temp_row.pickup_ou
700                 AND item_owning_ou      IS NOT DISTINCT FROM temp_row.item_owning_ou
701                 AND item_circ_ou        IS NOT DISTINCT FROM temp_row.item_circ_ou
702                 AND usr_grp             IS NOT DISTINCT FROM temp_row.usr_grp
703                 AND requestor_grp       IS NOT DISTINCT FROM temp_row.requestor_grp
704                 AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
705                 AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
706                 AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
707                 AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
708                 AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
709                 AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag;
710     END LOOP;
711 END;
712 $func$ LANGUAGE plpgsql;
713
714 SELECT action.cleanup_matrix_matchpoints();
715
716 DROP FUNCTION IF EXISTS action.cleanup_matrix_matchpoints();
717
718 -- Create Normalized indexes
719
720 CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, '')) WHERE active;
721
722 CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, '')) WHERE active;
723
724 -- 0484
725 DROP FUNCTION asset.metarecord_copy_count ( INT, BIGINT, BOOL );
726 DROP FUNCTION asset.record_copy_count ( INT, BIGINT, BOOL );
727
728 DROP FUNCTION asset.opac_ou_record_copy_count (INT, BIGINT);
729 CREATE OR REPLACE FUNCTION asset.opac_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
730 DECLARE
731     ans RECORD;
732     trans INT;
733 BEGIN
734     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
735
736     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
737         RETURN QUERY
738         SELECT  ans.depth,
739                 ans.id,
740                 COUNT( av.id ),
741                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
742                 COUNT( av.id ),
743                 trans
744           FROM  
745                 actor.org_unit_descendants(ans.id) d
746                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
747                 JOIN asset.copy cp ON (cp.id = av.copy_id)
748           GROUP BY 1,2,6;
749
750         IF NOT FOUND THEN
751             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
752         END IF;
753
754     END LOOP;
755
756     RETURN;
757 END;
758 $f$ LANGUAGE PLPGSQL;
759
760 DROP FUNCTION asset.opac_lasso_record_copy_count (INT, BIGINT);
761 CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
762 DECLARE
763     ans RECORD;
764     trans INT;
765 BEGIN
766     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
767
768     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
769         RETURN QUERY
770         SELECT  -1,
771                 ans.id,
772                 COUNT( av.id ),
773                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
774                 COUNT( av.id ),
775                 trans
776           FROM  
777                 actor.org_unit_descendants(ans.id) d
778                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
779                 JOIN asset.copy cp ON (cp.id = av.copy_id)
780           GROUP BY 1,2,6;
781
782         IF NOT FOUND THEN
783             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
784         END IF;
785
786     END LOOP;
787
788     RETURN;
789 END;
790 $f$ LANGUAGE PLPGSQL;
791
792 DROP FUNCTION asset.staff_ou_record_copy_count (INT, BIGINT);
793
794 DROP FUNCTION asset.staff_lasso_record_copy_count (INT, BIGINT);
795
796 CREATE OR REPLACE FUNCTION asset.record_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
797 BEGIN
798     IF staff IS TRUE THEN
799         IF place > 0 THEN
800             RETURN QUERY SELECT * FROM asset.staff_ou_record_copy_count( place, rid );
801         ELSE
802             RETURN QUERY SELECT * FROM asset.staff_lasso_record_copy_count( -place, rid );
803         END IF;
804     ELSE
805         IF place > 0 THEN
806             RETURN QUERY SELECT * FROM asset.opac_ou_record_copy_count( place, rid );
807         ELSE
808             RETURN QUERY SELECT * FROM asset.opac_lasso_record_copy_count( -place, rid );
809         END IF;
810     END IF;
811
812     RETURN;
813 END;
814 $f$ LANGUAGE PLPGSQL;
815
816 DROP FUNCTION asset.opac_ou_metarecord_copy_count (INT, BIGINT);
817 CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
818 DECLARE
819     ans RECORD;
820     trans INT;
821 BEGIN
822     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
823
824     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
825         RETURN QUERY
826         SELECT  ans.depth,
827                 ans.id,
828                 COUNT( av.id ),
829                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
830                 COUNT( av.id ),
831                 trans
832           FROM
833                 actor.org_unit_descendants(ans.id) d
834                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
835                 JOIN asset.copy cp ON (cp.id = av.copy_id)
836                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
837           GROUP BY 1,2,6;
838
839         IF NOT FOUND THEN
840             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
841         END IF;
842
843     END LOOP;
844
845     RETURN;
846 END;
847 $f$ LANGUAGE PLPGSQL;
848
849 DROP FUNCTION asset.opac_lasso_metarecord_copy_count (INT, BIGINT);
850 CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
851 DECLARE
852     ans RECORD;
853     trans INT;
854 BEGIN
855     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
856
857     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
858         RETURN QUERY
859         SELECT  -1,
860                 ans.id,
861                 COUNT( av.id ),
862                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
863                 COUNT( av.id ),
864                 trans
865           FROM
866                 actor.org_unit_descendants(ans.id) d
867                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
868                 JOIN asset.copy cp ON (cp.id = av.copy_id)
869                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
870           GROUP BY 1,2,6;
871
872         IF NOT FOUND THEN
873             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
874         END IF;
875
876     END LOOP;
877
878     RETURN;
879 END;
880 $f$ LANGUAGE PLPGSQL;
881
882 DROP FUNCTION asset.staff_lasso_metarecord_copy_count (INT, BIGINT);
883
884 CREATE OR REPLACE FUNCTION asset.metarecord_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
885 BEGIN
886     IF staff IS TRUE THEN
887         IF place > 0 THEN
888             RETURN QUERY SELECT * FROM asset.staff_ou_metarecord_copy_count( place, rid );
889         ELSE
890             RETURN QUERY SELECT * FROM asset.staff_lasso_metarecord_copy_count( -place, rid );
891         END IF;
892     ELSE
893         IF place > 0 THEN
894             RETURN QUERY SELECT * FROM asset.opac_ou_metarecord_copy_count( place, rid );
895         ELSE
896             RETURN QUERY SELECT * FROM asset.opac_lasso_metarecord_copy_count( -place, rid );
897         END IF;
898     END IF;
899
900     RETURN;
901 END;
902 $f$ LANGUAGE PLPGSQL;
903
904 -- 0485
905 CREATE OR REPLACE VIEW reporter.simple_record AS
906 SELECT  r.id,
907         s.metarecord,
908         r.fingerprint,
909         r.quality,
910         r.tcn_source,
911         r.tcn_value,
912         title.value AS title,
913         uniform_title.value AS uniform_title,
914         author.value AS author,
915         publisher.value AS publisher,
916         SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
917         series_title.value AS series_title,
918         series_statement.value AS series_statement,
919         summary.value AS summary,
920         ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
921         ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
922         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
923         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
924         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
925         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
926         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
927         ARRAY((SELECT value FROM metabib.full_rec WHERE tag = '856' AND subfield IN ('3','y','u') AND record = r.id ORDER BY CASE WHEN subfield IN ('3','y') THEN 0 ELSE 1 END)) AS external_uri
928   FROM  biblio.record_entry r
929         JOIN metabib.metarecord_source_map s ON (s.source = r.id)
930         LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
931         LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
932         LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
933         LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
934         LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
935         LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
936         LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
937         LEFT JOIN metabib.full_rec series_title ON (r.id = series_title.record AND series_title.tag IN ('830','440') AND series_title.subfield = 'a')
938         LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
939         LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
940   GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
941
942 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
943 SELECT  r.id,
944     r.fingerprint,
945     r.quality,
946     r.tcn_source,
947     r.tcn_value,
948     FIRST(title.value) AS title,
949     FIRST(author.value) AS author,
950     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT publisher.value), ', ') AS publisher,
951     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$) ), ', ') AS pubdate,
952     ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
953     ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn
954   FROM  biblio.record_entry r
955     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
956     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
957     LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
958     LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
959     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
960     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
961   GROUP BY 1,2,3,4,5;
962
963 -- 0486
964 ALTER TABLE money.credit_card_payment ADD COLUMN cc_order_number TEXT;
965
966 -- Changing return types requires explicit dropping of old versions
967 DROP FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
968 DROP FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
969 DROP FUNCTION action.item_user_circ_test( INT, BIGINT, INT );
970 DROP FUNCTION action.item_user_renew_test( INT, BIGINT, INT );
971
972 -- New return types
973 CREATE TYPE action.found_circ_matrix_matchpoint AS ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] );
974
975 -- Helper function - For manual calling, it can be easier to pass in IDs instead of objects
976 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.found_circ_matrix_matchpoint AS $func$
977 DECLARE
978     item_object asset.copy%ROWTYPE;
979     user_object actor.usr%ROWTYPE;
980 BEGIN
981     SELECT INTO item_object * FROM asset.copy   WHERE id = match_item;
982     SELECT INTO user_object * FROM actor.usr    WHERE id = match_user;
983
984     RETURN QUERY SELECT * FROM action.find_circ_matrix_matchpoint( context_ou, item_object, user_object, renewal );
985 END;
986 $func$ LANGUAGE plpgsql;
987
988 CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL );
989
990 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
991 DECLARE
992     user_object             actor.usr%ROWTYPE;
993     standing_penalty        config.standing_penalty%ROWTYPE;
994     item_object             asset.copy%ROWTYPE;
995     item_status_object      config.copy_status%ROWTYPE;
996     item_location_object    asset.copy_location%ROWTYPE;
997     result                  action.circ_matrix_test_result;
998     circ_test               action.found_circ_matrix_matchpoint;
999     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
1000     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
1001     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
1002     hold_ratio              action.hold_stats%ROWTYPE;
1003     penalty_type            TEXT;
1004     items_out               INT;
1005     context_org_list        INT[];
1006     done                    BOOL := FALSE;
1007 BEGIN
1008     -- Assume success unless we hit a failure condition
1009     result.success := TRUE;
1010
1011     -- Fail if the user is BARRED
1012     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
1013
1014     -- Fail if we couldn't find the user 
1015     IF user_object.id IS NULL THEN
1016         result.fail_part := 'no_user';
1017         result.success := FALSE;
1018         done := TRUE;
1019         RETURN NEXT result;
1020         RETURN;
1021     END IF;
1022
1023     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
1024
1025     -- Fail if we couldn't find the item 
1026     IF item_object.id IS NULL THEN
1027         result.fail_part := 'no_item';
1028         result.success := FALSE;
1029         done := TRUE;
1030         RETURN NEXT result;
1031         RETURN;
1032     END IF;
1033
1034     IF user_object.barred IS TRUE THEN
1035         result.fail_part := 'actor.usr.barred';
1036         result.success := FALSE;
1037         done := TRUE;
1038         RETURN NEXT result;
1039     END IF;
1040
1041     -- Fail if the item can't circulate
1042     IF item_object.circulate IS FALSE THEN
1043         result.fail_part := 'asset.copy.circulate';
1044         result.success := FALSE;
1045         done := TRUE;
1046         RETURN NEXT result;
1047     END IF;
1048
1049     -- Fail if the item isn't in a circulateable status on a non-renewal
1050     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
1051         result.fail_part := 'asset.copy.status';
1052         result.success := FALSE;
1053         done := TRUE;
1054         RETURN NEXT result;
1055     ELSIF renewal AND item_object.status <> 1 THEN
1056         result.fail_part := 'asset.copy.status';
1057         result.success := FALSE;
1058         done := TRUE;
1059         RETURN NEXT result;
1060     END IF;
1061
1062     -- Fail if the item can't circulate because of the shelving location
1063     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
1064     IF item_location_object.circulate IS FALSE THEN
1065         result.fail_part := 'asset.copy_location.circulate';
1066         result.success := FALSE;
1067         done := TRUE;
1068         RETURN NEXT result;
1069     END IF;
1070
1071     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
1072
1073     circ_matchpoint             := circ_test.matchpoint;
1074     result.matchpoint           := circ_matchpoint.id;
1075     result.circulate            := circ_matchpoint.circulate;
1076     result.duration_rule        := circ_matchpoint.duration_rule;
1077     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
1078     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
1079     result.hard_due_date        := circ_matchpoint.hard_due_date;
1080     result.renewals             := circ_matchpoint.renewals;
1081     result.buildrows            := circ_test.buildrows;
1082
1083     -- Fail if we couldn't find a matchpoint
1084     IF circ_test.success = false THEN
1085         result.fail_part := 'no_matchpoint';
1086         result.success := FALSE;
1087         done := TRUE;
1088         RETURN NEXT result;
1089         RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
1090     END IF;
1091
1092     -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
1093     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
1094
1095     IF renewal THEN
1096         penalty_type = '%RENEW%';
1097     ELSE
1098         penalty_type = '%CIRC%';
1099     END IF;
1100
1101     FOR standing_penalty IN
1102         SELECT  DISTINCT csp.*
1103           FROM  actor.usr_standing_penalty usp
1104                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1105           WHERE usr = match_user
1106                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1107                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1108                 AND csp.block_list LIKE penalty_type LOOP
1109
1110         result.fail_part := standing_penalty.name;
1111         result.success := FALSE;
1112         done := TRUE;
1113         RETURN NEXT result;
1114     END LOOP;
1115
1116     -- Fail if the test is set to hard non-circulating
1117     IF circ_matchpoint.circulate IS FALSE THEN
1118         result.fail_part := 'config.circ_matrix_test.circulate';
1119         result.success := FALSE;
1120         done := TRUE;
1121         RETURN NEXT result;
1122     END IF;
1123
1124     -- Fail if the total copy-hold ratio is too low
1125     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
1126         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
1127         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
1128             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
1129             result.success := FALSE;
1130             done := TRUE;
1131             RETURN NEXT result;
1132         END IF;
1133     END IF;
1134
1135     -- Fail if the available copy-hold ratio is too low
1136     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
1137         IF hold_ratio.hold_count IS NULL THEN
1138             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
1139         END IF;
1140         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
1141             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
1142             result.success := FALSE;
1143             done := TRUE;
1144             RETURN NEXT result;
1145         END IF;
1146     END IF;
1147
1148     -- Fail if the user has too many items with specific circ_modifiers checked out
1149     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
1150         SELECT  INTO items_out COUNT(*)
1151           FROM  action.circulation circ
1152             JOIN asset.copy cp ON (cp.id = circ.target_copy)
1153           WHERE circ.usr = match_user
1154                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
1155             AND circ.checkin_time IS NULL
1156             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
1157             AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
1158         IF items_out >= out_by_circ_mod.items_out THEN
1159             result.fail_part := 'config.circ_matrix_circ_mod_test';
1160             result.success := FALSE;
1161             done := TRUE;
1162             RETURN NEXT result;
1163         END IF;
1164     END LOOP;
1165
1166     -- If we passed everything, return the successful matchpoint id
1167     IF NOT done THEN
1168         RETURN NEXT result;
1169     END IF;
1170
1171     RETURN;
1172 END;
1173 $func$ LANGUAGE plpgsql;
1174
1175 CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
1176     SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
1177 $func$ LANGUAGE SQL;
1178
1179 CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
1180     SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
1181 $func$ LANGUAGE SQL;
1182
1183 -- 0490
1184 CREATE OR REPLACE FUNCTION asset.staff_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
1185 DECLARE         
1186     ans RECORD; 
1187     trans INT;
1188 BEGIN           
1189     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
1190
1191     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
1192         RETURN QUERY
1193         SELECT  ans.depth,
1194                 ans.id,
1195                 COUNT( cp.id ),
1196                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1197                 COUNT( cp.id ),
1198                 trans
1199           FROM
1200                 actor.org_unit_descendants(ans.id) d
1201                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1202                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1203           GROUP BY 1,2,6;
1204
1205         IF NOT FOUND THEN
1206             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1207         END IF;
1208
1209     END LOOP;
1210
1211     RETURN;
1212 END;
1213 $f$ LANGUAGE PLPGSQL;
1214
1215 CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
1216 DECLARE
1217     ans RECORD;
1218     trans INT;
1219 BEGIN
1220     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
1221
1222     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
1223         RETURN QUERY
1224         SELECT  -1,
1225                 ans.id,
1226                 COUNT( cp.id ),
1227                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1228                 COUNT( cp.id ),
1229                 trans
1230           FROM
1231                 actor.org_unit_descendants(ans.id) d
1232                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1233                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1234           GROUP BY 1,2,6;
1235
1236         IF NOT FOUND THEN
1237             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1238         END IF;
1239
1240     END LOOP;
1241
1242     RETURN;
1243 END;
1244 $f$ LANGUAGE PLPGSQL;
1245
1246 DROP FUNCTION asset.staff_ou_metarecord_copy_count (INT, BIGINT);
1247 CREATE OR REPLACE FUNCTION asset.staff_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
1248 DECLARE         
1249     ans RECORD; 
1250     trans INT;
1251 BEGIN
1252     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
1253
1254     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
1255         RETURN QUERY
1256         SELECT  ans.depth,
1257                 ans.id,
1258                 COUNT( cp.id ),
1259                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1260                 COUNT( cp.id ),
1261                 trans
1262           FROM
1263                 actor.org_unit_descendants(ans.id) d
1264                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1265                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1266                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
1267           GROUP BY 1,2,6;
1268
1269         IF NOT FOUND THEN
1270             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1271         END IF;
1272
1273     END LOOP;
1274
1275     RETURN;
1276 END;
1277 $f$ LANGUAGE PLPGSQL;
1278
1279 CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
1280 DECLARE
1281     ans RECORD;
1282     trans INT;
1283 BEGIN
1284     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
1285
1286     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
1287         RETURN QUERY
1288         SELECT  -1,
1289                 ans.id,
1290                 COUNT( cp.id ),
1291                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1292                 COUNT( cp.id ),
1293                 trans
1294           FROM
1295                 actor.org_unit_descendants(ans.id) d
1296                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1297                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1298                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
1299           GROUP BY 1,2,6;
1300
1301         IF NOT FOUND THEN
1302             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1303         END IF;
1304
1305     END LOOP;
1306
1307     RETURN;
1308 END;
1309 $f$ LANGUAGE PLPGSQL;
1310
1311
1312 -- 0493
1313 UPDATE config.org_unit_setting_type
1314     SET description = 'Amount of time before a hold expires at which point the patron should be alerted. Examples: "5 days", "1 hour"'
1315     WHERE label = 'Holds: Expire Alert Interval';
1316
1317 UPDATE config.org_unit_setting_type
1318     SET description = 'When predicting the amount of time a patron will be waiting for a hold to be fulfilled, this is the default estimated length of time to assume an item will be checked out. Examples: "3 weeks", "7 days"'
1319     WHERE label = 'Holds: Default Estimated Wait';
1320
1321 UPDATE config.org_unit_setting_type
1322     SET description = 'When predicting the amount of time a patron will be waiting for a hold to be fulfilled, this is the minimum estimated length of time to assume an item will be checked out. Examples: "1 week", "5 days"'
1323     WHERE label = 'Holds: Minimum Estimated Wait';
1324
1325 UPDATE config.org_unit_setting_type
1326     SET description = 'The purpose is to provide an interval of time after an item goes into the on-holds-shelf status before it appears to patrons that it is actually on the holds shelf.  This gives staff time to process the item before it shows as ready-for-pickup. Examples: "5 days", "1 hour"'
1327     WHERE label = 'Hold Shelf Status Delay';
1328
1329 -- 0494
1330 UPDATE config.metabib_field
1331     SET xpath = $$//mods32:mods/mods32:subject$$
1332     WHERE field_class = 'subject' AND name = 'complete';
1333
1334 UPDATE config.metabib_field
1335     SET xpath = $$//marc:datafield[@tag='099']$$
1336     WHERE field_class = 'identifier' AND name = 'bibcn';
1337
1338 -- 0495
1339 CREATE TABLE config.record_attr_definition (
1340     name        TEXT    PRIMARY KEY,
1341     label       TEXT    NOT NULL, -- I18N
1342     description TEXT,
1343     filter      BOOL    NOT NULL DEFAULT TRUE,  -- becomes QP filter if true
1344     sorter      BOOL    NOT NULL DEFAULT FALSE, -- becomes QP sort() axis if true
1345
1346 -- For pre-extracted fields. Takes the first occurance, uses naive subfield ordering
1347     tag         TEXT, -- LIKE format
1348     sf_list     TEXT, -- pile-o-values, like 'abcd' for a and b and c and d
1349
1350 -- This is used for both tag/sf and xpath entries
1351     joiner      TEXT,
1352
1353 -- For xpath-extracted attrs
1354     xpath       TEXT,
1355     format      TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1356     start_pos   INT,
1357     string_len  INT,
1358
1359 -- For fixed fields
1360     fixed_field TEXT, -- should exist in config.marc21_ff_pos_map.fixed_field
1361
1362 -- For phys-char fields
1363     phys_char_sf    INT REFERENCES config.marc21_physical_characteristic_subfield_map (id)
1364 );
1365
1366 CREATE TABLE config.record_attr_index_norm_map (
1367     id      SERIAL  PRIMARY KEY,
1368     attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1369     norm    INT     NOT NULL REFERENCES config.index_normalizer (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1370     params  TEXT,
1371     pos     INT     NOT NULL DEFAULT 0
1372 );
1373
1374 CREATE TABLE config.coded_value_map (
1375     id          SERIAL  PRIMARY KEY,
1376     ctype       TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1377     code        TEXT    NOT NULL,
1378     value       TEXT    NOT NULL,
1379     description TEXT
1380 );
1381
1382 -- record attributes
1383 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('alph','Alph','Alph');
1384 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('audience','Audn','Audn');
1385 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('bib_level','BLvl','BLvl');
1386 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('biog','Biog','Biog');
1387 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('conf','Conf','Conf');
1388 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('control_type','Ctrl','Ctrl');
1389 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ctry','Ctry','Ctry');
1390 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date1','Date1','Date1');
1391 INSERT INTO config.record_attr_definition (name,label,fixed_field,sorter,filter) values ('pubdate','Pub Date','Date1',TRUE,FALSE);
1392 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date2','Date2','Date2');
1393 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('cat_form','Desc','Desc');
1394 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('pub_status','DtSt','DtSt');
1395 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('enc_level','ELvl','ELvl');
1396 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('fest','Fest','Fest');
1397 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_form','Form','Form');
1398 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('gpub','GPub','GPub');
1399 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ills','Ills','Ills');
1400 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('indx','Indx','Indx');
1401 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_lang','Lang','Lang');
1402 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('lit_form','LitF','LitF');
1403 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('mrec','MRec','MRec');
1404 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ff_sl','S/L','S/L');
1405 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('type_mat','TMat','TMat');
1406 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_type','Type','Type');
1407 INSERT INTO config.record_attr_definition (name,label,phys_char_sf) values ('vr_format','Videorecording format',72);
1408 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('titlesort','Title',TRUE,FALSE,'tnf');
1409 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('authorsort','Author',TRUE,FALSE,'1%');
1410
1411 INSERT INTO config.upgrade_log (version) VALUES ('0624'); -- miker/tsbere
1412 -- Cont was typod as Conf. Update the old entries.
1413 UPDATE config.marc21_ff_pos_map SET fixed_field = 'Cont' WHERE fixed_field = 'Conf' AND length > 1;
1414 -- Conf thus didn't exist. Add it.
1415 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '006', 'BKS', 11, 1, ' ');
1416 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '006', 'SER', 11, 1, ' ');
1417 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '008', 'BKS', 29, 1, ' ');
1418 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '008', 'SER', 29, 1, ' ');
1419
1420 INSERT INTO config.coded_value_map (ctype,code,value,description)
1421     SELECT 'item_lang' AS ctype, code, value, NULL FROM config.language_map
1422         UNION
1423     SELECT 'bib_level' AS ctype, code, value, NULL FROM config.bib_level_map
1424         UNION
1425     SELECT 'item_form' AS ctype, code, value, NULL FROM config.item_form_map
1426         UNION
1427     SELECT 'item_type' AS ctype, code, value, NULL FROM config.item_type_map
1428         UNION
1429     SELECT 'lit_form' AS ctype, code, value, description FROM config.lit_form_map
1430         UNION
1431     SELECT 'audience' AS ctype, code, value, description FROM config.audience_map
1432         UNION
1433     SELECT 'vr_format' AS ctype, code, value, NULL FROM config.videorecording_format_map;
1434
1435 ALTER TABLE config.i18n_locale DROP CONSTRAINT i18n_locale_marc_code_fkey;
1436
1437 DROP TABLE config.language_map;
1438 DROP TABLE config.bib_level_map;
1439 DROP TABLE config.item_form_map;
1440 DROP TABLE config.item_type_map;
1441 DROP TABLE config.lit_form_map;
1442 DROP TABLE config.audience_map;
1443 DROP TABLE config.videorecording_format_map;
1444
1445 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clm.value' AND ccvm.ctype = 'item_lang' AND identity_value = ccvm.code;
1446 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cblvl.value' AND ccvm.ctype = 'bib_level' AND identity_value = ccvm.code;
1447 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cifm.value' AND ccvm.ctype = 'item_form' AND identity_value = ccvm.code;
1448 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'citm.value' AND ccvm.ctype = 'item_type' AND identity_value = ccvm.code;
1449 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clfm.value' AND ccvm.ctype = 'lit_form' AND identity_value = ccvm.code;
1450 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cam.value' AND ccvm.ctype = 'audience' AND identity_value = ccvm.code;
1451 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cvrfm.value' AND ccvm.ctype = 'vr_format' AND identity_value = ccvm.code;
1452
1453 UPDATE config.i18n_core SET fq_field = 'ccvm.description', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clfm.description' AND ccvm.ctype = 'lit_form' AND identity_value = ccvm.code;
1454 UPDATE config.i18n_core SET fq_field = 'ccvm.description', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cam.description' AND ccvm.ctype = 'audience' AND identity_value = ccvm.code;
1455
1456 CREATE VIEW config.language_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_lang';
1457 CREATE VIEW config.bib_level_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'bib_level';
1458 CREATE VIEW config.item_form_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_form';
1459 CREATE VIEW config.item_type_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_type';
1460 CREATE VIEW config.lit_form_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'lit_form';
1461 CREATE VIEW config.audience_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'audience';
1462 CREATE VIEW config.videorecording_format_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'vr_format';
1463
1464 CREATE TABLE metabib.record_attr (
1465        id              BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
1466        attrs   HSTORE  NOT NULL DEFAULT ''::HSTORE
1467 );
1468 CREATE INDEX metabib_svf_attrs_idx ON metabib.record_attr USING GIST (attrs);
1469 CREATE INDEX metabib_svf_date1_idx ON metabib.record_attr ( (attrs->'date1') );
1470 CREATE INDEX metabib_svf_dates_idx ON metabib.record_attr ( (attrs->'date1'), (attrs->'date2') );
1471
1472 INSERT INTO metabib.record_attr (id,attrs)
1473     SELECT DISTINCT ON (mrd.record) mrd.record, hstore(mrd) - '{id,record}'::TEXT[] FROM metabib.rec_descriptor mrd;
1474
1475 -- Back-compat view ... we're moving to an HSTORE world
1476 CREATE TYPE metabib.rec_desc_type AS (
1477     item_type       TEXT,
1478     item_form       TEXT,
1479     bib_level       TEXT,
1480     control_type    TEXT,
1481     char_encoding   TEXT,
1482     enc_level       TEXT,
1483     audience        TEXT,
1484     lit_form        TEXT,
1485     type_mat        TEXT,
1486     cat_form        TEXT,
1487     pub_status      TEXT,
1488     item_lang       TEXT,
1489     vr_format       TEXT,
1490     date1           TEXT,
1491     date2           TEXT
1492 );
1493
1494 DROP TABLE metabib.rec_descriptor CASCADE;
1495
1496 CREATE VIEW metabib.rec_descriptor AS
1497     SELECT  id,
1498             id AS record,
1499             (populate_record(NULL::metabib.rec_desc_type, attrs)).*
1500       FROM  metabib.record_attr;
1501
1502 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
1503 DECLARE
1504     ldr         TEXT;
1505     tval        TEXT;
1506     tval_rec    RECORD;
1507     bval        TEXT;
1508     bval_rec    RECORD;
1509     retval      config.marc21_rec_type_map%ROWTYPE;
1510 BEGIN
1511     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
1512
1513     IF ldr IS NULL OR ldr = '' THEN
1514         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1515         RETURN retval;
1516     END IF;
1517
1518     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
1519     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
1520
1521
1522     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1523     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1524
1525     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1526
1527     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1528
1529
1530     IF retval.code IS NULL THEN
1531         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1532     END IF;
1533
1534     RETURN retval;
1535 END;
1536 $func$ LANGUAGE PLPGSQL;
1537
1538 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
1539     SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1540 $func$ LANGUAGE SQL;
1541
1542 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
1543 DECLARE
1544     rtype       TEXT;
1545     ff_pos      RECORD;
1546     tag_data    RECORD;
1547     val         TEXT;
1548 BEGIN
1549     rtype := (vandelay.marc21_record_type( marc )).code;
1550     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
1551         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1552             val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1553             RETURN val;
1554         END LOOP;
1555         val := REPEAT( ff_pos.default_val, ff_pos.length );
1556         RETURN val;
1557     END LOOP;
1558
1559     RETURN NULL;
1560 END;
1561 $func$ LANGUAGE PLPGSQL;
1562
1563 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
1564     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
1565 $func$ LANGUAGE SQL;
1566
1567 CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
1568 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
1569 DECLARE
1570     tag_data    TEXT;
1571     rtype       TEXT;
1572     ff_pos      RECORD;
1573     output      biblio.record_ff_map%ROWTYPE;
1574 BEGIN
1575     rtype := (vandelay.marc21_record_type( marc )).code;
1576
1577     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
1578         output.ff_name  := ff_pos.fixed_field;
1579         output.ff_value := NULL;
1580
1581         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(tag) || '"]/text()', marc ) ) x(value) LOOP
1582             output.ff_value := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1583             IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
1584             RETURN NEXT output;
1585             output.ff_value := NULL;
1586         END LOOP;
1587
1588     END LOOP;
1589
1590     RETURN;
1591 END;
1592 $func$ LANGUAGE PLPGSQL;
1593
1594 CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
1595     SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1596 $func$ LANGUAGE SQL;
1597
1598 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1599 DECLARE
1600     rowid   INT := 0;
1601     _007    TEXT;
1602     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
1603     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
1604     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
1605     retval  biblio.marc21_physical_characteristics%ROWTYPE;
1606 BEGIN
1607
1608     _007 := oils_xpath_string( '//*[@tag="007"]', marc );
1609
1610     IF _007 IS NOT NULL AND _007 <> '' THEN
1611         SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
1612
1613         IF ptype.ptype_key IS NOT NULL THEN
1614             FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
1615                 SELECT * INTO pval FROM config.marc21_physical_characteristic_value_map WHERE ptype_subfield = psf.id AND value = SUBSTRING( _007, psf.start_pos + 1, psf.length );
1616
1617                 IF pval.id IS NOT NULL THEN
1618                     rowid := rowid + 1;
1619                     retval.id := rowid;
1620                     retval.ptype := ptype.ptype_key;
1621                     retval.subfield := psf.id;
1622                     retval.value := pval.id;
1623                     RETURN NEXT retval;
1624                 END IF;
1625
1626             END LOOP;
1627         END IF;
1628     END IF;
1629
1630     RETURN;
1631 END;
1632 $func$ LANGUAGE PLPGSQL;
1633
1634 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1635     SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1636 $func$ LANGUAGE SQL;
1637
1638 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
1639 DECLARE
1640     transformed_xml TEXT;
1641     prev_xfrm       TEXT;
1642     normalizer      RECORD;
1643     xfrm            config.xml_transform%ROWTYPE;
1644     attr_value      TEXT;
1645     new_attrs       HSTORE := ''::HSTORE;
1646     attr_def        config.record_attr_definition%ROWTYPE;
1647 BEGIN
1648
1649     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
1650         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
1651         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
1652         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
1653         RETURN NEW; -- and we're done
1654     END IF;
1655
1656     IF TG_OP = 'UPDATE' THEN -- re-ingest?
1657         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
1658
1659         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
1660             RETURN NEW;
1661         END IF;
1662     END IF;
1663
1664     -- Record authority linking
1665     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
1666     IF NOT FOUND THEN
1667         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
1668     END IF;
1669
1670     -- Flatten and insert the mfr data
1671     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
1672     IF NOT FOUND THEN
1673         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
1674
1675         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
1676         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
1677         IF NOT FOUND THEN
1678             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
1679
1680                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1681                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
1682                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
1683                       WHERE record = NEW.id
1684                             AND tag LIKE attr_def.tag
1685                             AND CASE
1686                                 WHEN attr_def.sf_list IS NOT NULL
1687                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
1688                                 ELSE TRUE
1689                                 END
1690                       GROUP BY tag
1691                       ORDER BY tag
1692                       LIMIT 1;
1693
1694                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1695                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
1696
1697                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1698
1699                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1700
1701                     -- See if we can skip the XSLT ... it's expensive
1702                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1703                         -- Can't skip the transform
1704                         IF xfrm.xslt <> '---' THEN
1705                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
1706                         ELSE
1707                             transformed_xml := NEW.marc;
1708                         END IF;
1709
1710                         prev_xfrm := xfrm.name;
1711                     END IF;
1712
1713                     IF xfrm.name IS NULL THEN
1714                         -- just grab the marcxml (empty) transform
1715                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1716                         prev_xfrm := xfrm.name;
1717                     END IF;
1718
1719                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
1720
1721                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1722                     SELECT  value::TEXT INTO attr_value
1723                       FROM  biblio.marc21_physical_characteristics(NEW.id)
1724                       WHERE subfield = attr_def.phys_char_sf
1725                       LIMIT 1; -- Just in case ...
1726
1727                 END IF;
1728
1729                 -- apply index normalizers to attr_value
1730                 FOR normalizer IN
1731                     SELECT  n.func AS func,
1732                             n.param_count AS param_count,
1733                             m.params AS params
1734                       FROM  config.index_normalizer n
1735                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1736                       WHERE attr = attr_def.name
1737                       ORDER BY m.pos LOOP
1738                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
1739                             quote_literal( attr_value ) ||
1740                             CASE
1741                                 WHEN normalizer.param_count > 0
1742                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1743                                     ELSE ''
1744                                 END ||
1745                             ')' INTO attr_value;
1746
1747                 END LOOP;
1748
1749                 -- Add the new value to the hstore
1750                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
1751
1752             END LOOP;
1753
1754             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
1755                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
1756             ELSE
1757                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
1758             END IF;
1759
1760         END IF;
1761     END IF;
1762
1763     -- Gather and insert the field entry data
1764     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
1765
1766     -- Located URI magic
1767     IF TG_OP = 'INSERT' THEN
1768         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1769         IF NOT FOUND THEN
1770             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1771         END IF;
1772     ELSE
1773         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1774         IF NOT FOUND THEN
1775             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1776         END IF;
1777     END IF;
1778
1779     -- (re)map metarecord-bib linking
1780     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
1781         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
1782         IF NOT FOUND THEN
1783             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1784         END IF;
1785     ELSE -- we're doing an update, and we're not deleted, remap
1786         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
1787         IF NOT FOUND THEN
1788             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1789         END IF;
1790     END IF;
1791
1792     RETURN NEW;
1793 END;
1794 $func$ LANGUAGE PLPGSQL;
1795
1796 DROP FUNCTION metabib.reingest_metabib_rec_descriptor( bib_id BIGINT );
1797
1798 CREATE OR REPLACE FUNCTION public.approximate_date( TEXT, TEXT ) RETURNS TEXT AS $func$
1799         SELECT REGEXP_REPLACE( $1, E'\\D', $2, 'g' );
1800 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1801
1802 CREATE OR REPLACE FUNCTION public.approximate_low_date( TEXT ) RETURNS TEXT AS $func$
1803         SELECT approximate_date( $1, '0');
1804 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1805
1806 CREATE OR REPLACE FUNCTION public.approximate_high_date( TEXT ) RETURNS TEXT AS $func$
1807         SELECT approximate_date( $1, '9');
1808 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1809
1810 CREATE OR REPLACE FUNCTION public.integer_or_null( TEXT ) RETURNS TEXT AS $func$
1811         SELECT CASE WHEN $1 ~ E'^\\d+$' THEN $1 ELSE NULL END
1812 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1813
1814 CREATE OR REPLACE FUNCTION public.content_or_null( TEXT ) RETURNS TEXT AS $func$
1815         SELECT CASE WHEN $1 ~ E'^\\s*$' THEN NULL ELSE $1 END
1816 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1817
1818 CREATE OR REPLACE FUNCTION public.force_to_isbn13( TEXT ) RETURNS TEXT AS $func$
1819     use Business::ISBN;
1820     use strict;
1821     use warnings;
1822
1823     # Find the first ISBN, force it to ISBN13 and return it
1824
1825     my $input = shift;
1826
1827     foreach my $word (split(/\s/, $input)) {
1828         my $isbn = Business::ISBN->new($word);
1829
1830         # First check the checksum; if it is not valid, fix it and add the original
1831         # bad-checksum ISBN to the output
1832         if ($isbn && $isbn->is_valid_checksum() == Business::ISBN::BAD_CHECKSUM) {
1833             $isbn->fix_checksum();
1834         }
1835
1836         # If we now have a valid ISBN, force it to ISBN13 and return it
1837         return $isbn->as_isbn13->isbn if ($isbn && $isbn->is_valid());
1838     }
1839     return undef;
1840 $func$ LANGUAGE PLPERLU;
1841
1842 COMMENT ON FUNCTION public.force_to_isbn13(TEXT) IS $$
1843 /*
1844  * Copyright (C) 2011 Equinox Software
1845  * Mike Rylander <mrylander@gmail.com>
1846  *
1847  * Inspired by translate_isbn1013
1848  *
1849  * The force_to_isbn13 function takes an input ISBN and returns the ISBN13
1850  * version without hypens and with a repaired checksum if the checksum was bad
1851  */
1852 $$;
1853
1854 -- 0496
1855 UPDATE config.metabib_field
1856     SET xpath = $$//marc:datafield[@tag='024' and @ind1='1']/marc:subfield[@code='a' or @code='z']$$
1857     WHERE field_class = 'identifier' AND name = 'upc';
1858
1859 UPDATE config.metabib_field
1860     SET xpath = $$//marc:datafield[@tag='024' and @ind1='2']/marc:subfield[@code='a' or @code='z']$$
1861     WHERE field_class = 'identifier' AND name = 'ismn';
1862
1863 UPDATE config.metabib_field
1864     SET xpath = $$//marc:datafield[@tag='024' and @ind1='3']/marc:subfield[@code='a' or @code='z']$$
1865     WHERE field_class = 'identifier' AND name = 'ean';
1866
1867 UPDATE config.metabib_field
1868     SET xpath = $$//marc:datafield[@tag='024' and @ind1='0']/marc:subfield[@code='a' or @code='z']$$
1869     WHERE field_class = 'identifier' AND name = 'isrc';
1870
1871 UPDATE config.metabib_field
1872     SET xpath = $$//marc:datafield[@tag='024' and @ind1='4']/marc:subfield[@code='a' or @code='z']$$
1873     WHERE field_class = 'identifier' AND name = 'sici';
1874
1875 -- 0497
1876 INSERT into config.org_unit_setting_type
1877 ( name, label, description, datatype ) VALUES
1878
1879 ( 'ui.patron.edit.au.active.show',
1880     oils_i18n_gettext('ui.patron.edit.au.active.show', 'GUI: Show active field on patron registration', 'coust', 'label'),
1881     oils_i18n_gettext('ui.patron.edit.au.active.show', 'The active field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1882     'bool'),
1883 ( 'ui.patron.edit.au.active.suggest',
1884     oils_i18n_gettext('ui.patron.edit.au.active.suggest', 'GUI: Suggest active field on patron registration', 'coust', 'label'),
1885     oils_i18n_gettext('ui.patron.edit.au.active.suggest', 'The active field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1886     'bool'),
1887 ( 'ui.patron.edit.au.alert_message.show',
1888     oils_i18n_gettext('ui.patron.edit.au.alert_message.show', 'GUI: Show alert_message field on patron registration', 'coust', 'label'),
1889     oils_i18n_gettext('ui.patron.edit.au.alert_message.show', 'The alert_message field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1890     'bool'),
1891 ( 'ui.patron.edit.au.alert_message.suggest',
1892     oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest', 'GUI: Suggest alert_message field on patron registration', 'coust', 'label'),
1893     oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest', 'The alert_message field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1894     'bool'),
1895 ( 'ui.patron.edit.au.alias.show',
1896     oils_i18n_gettext('ui.patron.edit.au.alias.show', 'GUI: Show alias field on patron registration', 'coust', 'label'),
1897     oils_i18n_gettext('ui.patron.edit.au.alias.show', 'The alias field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1898     'bool'),
1899 ( 'ui.patron.edit.au.alias.suggest',
1900     oils_i18n_gettext('ui.patron.edit.au.alias.suggest', 'GUI: Suggest alias field on patron registration', 'coust', 'label'),
1901     oils_i18n_gettext('ui.patron.edit.au.alias.suggest', 'The alias field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1902     'bool'),
1903 ( 'ui.patron.edit.au.barred.show',
1904     oils_i18n_gettext('ui.patron.edit.au.barred.show', 'GUI: Show barred field on patron registration', 'coust', 'label'),
1905     oils_i18n_gettext('ui.patron.edit.au.barred.show', 'The barred field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1906     'bool'),
1907 ( 'ui.patron.edit.au.barred.suggest',
1908     oils_i18n_gettext('ui.patron.edit.au.barred.suggest', 'GUI: Suggest barred field on patron registration', 'coust', 'label'),
1909     oils_i18n_gettext('ui.patron.edit.au.barred.suggest', 'The barred field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1910     'bool'),
1911 ( 'ui.patron.edit.au.claims_never_checked_out_count.show',
1912     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.show', 'GUI: Show claims_never_checked_out_count field on patron registration', 'coust', 'label'),
1913     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.show', 'The claims_never_checked_out_count field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1914     'bool'),
1915 ( 'ui.patron.edit.au.claims_never_checked_out_count.suggest',
1916     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.suggest', 'GUI: Suggest claims_never_checked_out_count field on patron registration', 'coust', 'label'),
1917     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.suggest', 'The claims_never_checked_out_count field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1918     'bool'),
1919 ( 'ui.patron.edit.au.claims_returned_count.show',
1920     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.show', 'GUI: Show claims_returned_count field on patron registration', 'coust', 'label'),
1921     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.show', 'The claims_returned_count field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1922     'bool'),
1923 ( 'ui.patron.edit.au.claims_returned_count.suggest',
1924     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.suggest', 'GUI: Suggest claims_returned_count field on patron registration', 'coust', 'label'),
1925     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.suggest', 'The claims_returned_count field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1926     'bool'),
1927 ( 'ui.patron.edit.au.day_phone.example',
1928     oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'GUI: Example for day_phone field on patron registration', 'coust', 'label'),
1929     oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'The Example for validation on the day_phone field in patron registration.', 'coust', 'description'),
1930     'string'),
1931 ( 'ui.patron.edit.au.day_phone.regex',
1932     oils_i18n_gettext('ui.patron.edit.au.day_phone.regex', 'GUI: Regex for day_phone field on patron registration', 'coust', 'label'),
1933     oils_i18n_gettext('ui.patron.edit.au.day_phone.regex', 'The Regular Expression for validation on the day_phone field in patron registration.', 'coust', 'description'),
1934     'string'),
1935 ( 'ui.patron.edit.au.day_phone.require',
1936     oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'GUI: Require day_phone field on patron registration', 'coust', 'label'),
1937     oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'The day_phone field will be required on the patron registration screen.', 'coust', 'description'),
1938     'bool'),
1939 ( 'ui.patron.edit.au.day_phone.show',
1940     oils_i18n_gettext('ui.patron.edit.au.day_phone.show', 'GUI: Show day_phone field on patron registration', 'coust', 'label'),
1941     oils_i18n_gettext('ui.patron.edit.au.day_phone.show', 'The day_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1942     'bool'),
1943 ( 'ui.patron.edit.au.day_phone.suggest',
1944     oils_i18n_gettext('ui.patron.edit.au.day_phone.suggest', 'GUI: Suggest day_phone field on patron registration', 'coust', 'label'),
1945     oils_i18n_gettext('ui.patron.edit.au.day_phone.suggest', 'The day_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1946     'bool'),
1947 ( 'ui.patron.edit.au.dob.calendar',
1948     oils_i18n_gettext('ui.patron.edit.au.dob.calendar', 'GUI: Show calendar widget for dob field on patron registration', 'coust', 'label'),
1949     oils_i18n_gettext('ui.patron.edit.au.dob.calendar', 'If set the calendar widget will appear when editing the dob field on the patron registration form.', 'coust', 'description'),
1950     'bool'),
1951 ( 'ui.patron.edit.au.dob.require',
1952     oils_i18n_gettext('ui.patron.edit.au.dob.require', 'GUI: Require dob field on patron registration', 'coust', 'label'),
1953     oils_i18n_gettext('ui.patron.edit.au.dob.require', 'The dob field will be required on the patron registration screen.', 'coust', 'description'),
1954     'bool'),
1955 ( 'ui.patron.edit.au.dob.show',
1956     oils_i18n_gettext('ui.patron.edit.au.dob.show', 'GUI: Show dob field on patron registration', 'coust', 'label'),
1957     oils_i18n_gettext('ui.patron.edit.au.dob.show', 'The dob field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1958     'bool'),
1959 ( 'ui.patron.edit.au.dob.suggest',
1960     oils_i18n_gettext('ui.patron.edit.au.dob.suggest', 'GUI: Suggest dob field on patron registration', 'coust', 'label'),
1961     oils_i18n_gettext('ui.patron.edit.au.dob.suggest', 'The dob field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1962     'bool'),
1963 ( 'ui.patron.edit.au.email.example',
1964     oils_i18n_gettext('ui.patron.edit.au.email.example', 'GUI: Example for email field on patron registration', 'coust', 'label'),
1965     oils_i18n_gettext('ui.patron.edit.au.email.example', 'The Example for validation on the email field in patron registration.', 'coust', 'description'),
1966     'string'),
1967 ( 'ui.patron.edit.au.email.regex',
1968     oils_i18n_gettext('ui.patron.edit.au.email.regex', 'GUI: Regex for email field on patron registration', 'coust', 'label'),
1969     oils_i18n_gettext('ui.patron.edit.au.email.regex', 'The Regular Expression for validation on the email field in patron registration.', 'coust', 'description'),
1970     'string'),
1971 ( 'ui.patron.edit.au.email.require',
1972     oils_i18n_gettext('ui.patron.edit.au.email.require', 'GUI: Require email field on patron registration', 'coust', 'label'),
1973     oils_i18n_gettext('ui.patron.edit.au.email.require', 'The email field will be required on the patron registration screen.', 'coust', 'description'),
1974     'bool'),
1975 ( 'ui.patron.edit.au.email.show',
1976     oils_i18n_gettext('ui.patron.edit.au.email.show', 'GUI: Show email field on patron registration', 'coust', 'label'),
1977     oils_i18n_gettext('ui.patron.edit.au.email.show', 'The email field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1978     'bool'),
1979 ( 'ui.patron.edit.au.email.suggest',
1980     oils_i18n_gettext('ui.patron.edit.au.email.suggest', 'GUI: Suggest email field on patron registration', 'coust', 'label'),
1981     oils_i18n_gettext('ui.patron.edit.au.email.suggest', 'The email field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1982     'bool'),
1983 ( 'ui.patron.edit.au.evening_phone.example',
1984     oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'GUI: Example for evening_phone field on patron registration', 'coust', 'label'),
1985     oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'The Example for validation on the evening_phone field in patron registration.', 'coust', 'description'),
1986     'string'),
1987 ( 'ui.patron.edit.au.evening_phone.regex',
1988     oils_i18n_gettext('ui.patron.edit.au.evening_phone.regex', 'GUI: Regex for evening_phone field on patron registration', 'coust', 'label'),
1989     oils_i18n_gettext('ui.patron.edit.au.evening_phone.regex', 'The Regular Expression for validation on the evening_phone field in patron registration.', 'coust', 'description'),
1990     'string'),
1991 ( 'ui.patron.edit.au.evening_phone.require',
1992     oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'GUI: Require evening_phone field on patron registration', 'coust', 'label'),
1993     oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'The evening_phone field will be required on the patron registration screen.', 'coust', 'description'),
1994     'bool'),
1995 ( 'ui.patron.edit.au.evening_phone.show',
1996     oils_i18n_gettext('ui.patron.edit.au.evening_phone.show', 'GUI: Show evening_phone field on patron registration', 'coust', 'label'),
1997     oils_i18n_gettext('ui.patron.edit.au.evening_phone.show', 'The evening_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1998     'bool'),
1999 ( 'ui.patron.edit.au.evening_phone.suggest',
2000     oils_i18n_gettext('ui.patron.edit.au.evening_phone.suggest', 'GUI: Suggest evening_phone field on patron registration', 'coust', 'label'),
2001     oils_i18n_gettext('ui.patron.edit.au.evening_phone.suggest', 'The evening_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2002     'bool'),
2003 ( 'ui.patron.edit.au.ident_value.show',
2004     oils_i18n_gettext('ui.patron.edit.au.ident_value.show', 'GUI: Show ident_value field on patron registration', 'coust', 'label'),
2005     oils_i18n_gettext('ui.patron.edit.au.ident_value.show', 'The ident_value field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2006     'bool'),
2007 ( 'ui.patron.edit.au.ident_value.suggest',
2008     oils_i18n_gettext('ui.patron.edit.au.ident_value.suggest', 'GUI: Suggest ident_value field on patron registration', 'coust', 'label'),
2009     oils_i18n_gettext('ui.patron.edit.au.ident_value.suggest', 'The ident_value field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2010     'bool'),
2011 ( 'ui.patron.edit.au.ident_value2.show',
2012     oils_i18n_gettext('ui.patron.edit.au.ident_value2.show', 'GUI: Show ident_value2 field on patron registration', 'coust', 'label'),
2013     oils_i18n_gettext('ui.patron.edit.au.ident_value2.show', 'The ident_value2 field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2014     'bool'),
2015 ( 'ui.patron.edit.au.ident_value2.suggest',
2016     oils_i18n_gettext('ui.patron.edit.au.ident_value2.suggest', 'GUI: Suggest ident_value2 field on patron registration', 'coust', 'label'),
2017     oils_i18n_gettext('ui.patron.edit.au.ident_value2.suggest', 'The ident_value2 field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2018     'bool'),
2019 ( 'ui.patron.edit.au.juvenile.show',
2020     oils_i18n_gettext('ui.patron.edit.au.juvenile.show', 'GUI: Show juvenile field on patron registration', 'coust', 'label'),
2021     oils_i18n_gettext('ui.patron.edit.au.juvenile.show', 'The juvenile field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2022     'bool'),
2023 ( 'ui.patron.edit.au.juvenile.suggest',
2024     oils_i18n_gettext('ui.patron.edit.au.juvenile.suggest', 'GUI: Suggest juvenile field on patron registration', 'coust', 'label'),
2025     oils_i18n_gettext('ui.patron.edit.au.juvenile.suggest', 'The juvenile field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2026     'bool'),
2027 ( 'ui.patron.edit.au.master_account.show',
2028     oils_i18n_gettext('ui.patron.edit.au.master_account.show', 'GUI: Show master_account field on patron registration', 'coust', 'label'),
2029     oils_i18n_gettext('ui.patron.edit.au.master_account.show', 'The master_account field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2030     'bool'),
2031 ( 'ui.patron.edit.au.master_account.suggest',
2032     oils_i18n_gettext('ui.patron.edit.au.master_account.suggest', 'GUI: Suggest master_account field on patron registration', 'coust', 'label'),
2033     oils_i18n_gettext('ui.patron.edit.au.master_account.suggest', 'The master_account field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2034     'bool'),
2035 ( 'ui.patron.edit.au.other_phone.example',
2036     oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'GUI: Example for other_phone field on patron registration', 'coust', 'label'),
2037     oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'The Example for validation on the other_phone field in patron registration.', 'coust', 'description'),
2038     'string'),
2039 ( 'ui.patron.edit.au.other_phone.regex',
2040     oils_i18n_gettext('ui.patron.edit.au.other_phone.regex', 'GUI: Regex for other_phone field on patron registration', 'coust', 'label'),
2041     oils_i18n_gettext('ui.patron.edit.au.other_phone.regex', 'The Regular Expression for validation on the other_phone field in patron registration.', 'coust', 'description'),
2042     'string'),
2043 ( 'ui.patron.edit.au.other_phone.require',
2044     oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'GUI: Require other_phone field on patron registration', 'coust', 'label'),
2045     oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'The other_phone field will be required on the patron registration screen.', 'coust', 'description'),
2046     'bool'),
2047 ( 'ui.patron.edit.au.other_phone.show',
2048     oils_i18n_gettext('ui.patron.edit.au.other_phone.show', 'GUI: Show other_phone field on patron registration', 'coust', 'label'),
2049     oils_i18n_gettext('ui.patron.edit.au.other_phone.show', 'The other_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2050     'bool'),
2051 ( 'ui.patron.edit.au.other_phone.suggest',
2052     oils_i18n_gettext('ui.patron.edit.au.other_phone.suggest', 'GUI: Suggest other_phone field on patron registration', 'coust', 'label'),
2053     oils_i18n_gettext('ui.patron.edit.au.other_phone.suggest', 'The other_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2054     'bool'),
2055 ( 'ui.patron.edit.au.second_given_name.show',
2056     oils_i18n_gettext('ui.patron.edit.au.second_given_name.show', 'GUI: Show second_given_name field on patron registration', 'coust', 'label'),
2057     oils_i18n_gettext('ui.patron.edit.au.second_given_name.show', 'The second_given_name field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2058     'bool'),
2059 ( 'ui.patron.edit.au.second_given_name.suggest',
2060     oils_i18n_gettext('ui.patron.edit.au.second_given_name.suggest', 'GUI: Suggest second_given_name field on patron registration', 'coust', 'label'),
2061     oils_i18n_gettext('ui.patron.edit.au.second_given_name.suggest', 'The second_given_name field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2062     'bool'),
2063 ( 'ui.patron.edit.au.suffix.show',
2064     oils_i18n_gettext('ui.patron.edit.au.suffix.show', 'GUI: Show suffix field on patron registration', 'coust', 'label'),
2065     oils_i18n_gettext('ui.patron.edit.au.suffix.show', 'The suffix field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2066     'bool'),
2067 ( 'ui.patron.edit.au.suffix.suggest',
2068     oils_i18n_gettext('ui.patron.edit.au.suffix.suggest', 'GUI: Suggest suffix field on patron registration', 'coust', 'label'),
2069     oils_i18n_gettext('ui.patron.edit.au.suffix.suggest', 'The suffix field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2070     'bool'),
2071 ( 'ui.patron.edit.aua.county.require',
2072     oils_i18n_gettext('ui.patron.edit.aua.county.require', 'GUI: Require county field on patron registration', 'coust', 'label'),
2073     oils_i18n_gettext('ui.patron.edit.aua.county.require', 'The county field will be required on the patron registration screen.', 'coust', 'description'),
2074     'bool'),
2075 ( 'ui.patron.edit.aua.post_code.example',
2076     oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'GUI: Example for post_code field on patron registration', 'coust', 'label'),
2077     oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'The Example for validation on the post_code field in patron registration.', 'coust', 'description'),
2078     'string'),
2079 ( 'ui.patron.edit.aua.post_code.regex',
2080     oils_i18n_gettext('ui.patron.edit.aua.post_code.regex', 'GUI: Regex for post_code field on patron registration', 'coust', 'label'),
2081     oils_i18n_gettext('ui.patron.edit.aua.post_code.regex', 'The Regular Expression for validation on the post_code field in patron registration.', 'coust', 'description'),
2082     'string'),
2083 ( 'ui.patron.edit.default_suggested',
2084     oils_i18n_gettext('ui.patron.edit.default_suggested', 'GUI: Default showing suggested patron registration fields', 'coust', 'label'),
2085     oils_i18n_gettext('ui.patron.edit.default_suggested', 'Instead of All fields, show just suggested fields in patron registration by default.', 'coust', 'description'),
2086     'bool'),
2087 ( 'ui.patron.edit.phone.example',
2088     oils_i18n_gettext('ui.patron.edit.phone.example', 'GUI: Example for phone fields on patron registration', 'coust', 'label'),
2089     oils_i18n_gettext('ui.patron.edit.phone.example', 'The Example for validation on phone fields in patron registration. Applies to all phone fields without their own setting.', 'coust', 'description'),
2090     'string'),
2091 ( 'ui.patron.edit.phone.regex',
2092     oils_i18n_gettext('ui.patron.edit.phone.regex', 'GUI: Regex for phone fields on patron registration', 'coust', 'label'),
2093     oils_i18n_gettext('ui.patron.edit.phone.regex', 'The Regular Expression for validation on phone fields in patron registration. Applies to all phone fields without their own setting.', 'coust', 'description'),
2094     'string');
2095
2096 -- update actor.usr_address indexes
2097 DROP INDEX IF EXISTS actor.actor_usr_addr_street1_idx;
2098 DROP INDEX IF EXISTS actor.actor_usr_addr_street2_idx;
2099 DROP INDEX IF EXISTS actor.actor_usr_addr_city_idx;
2100 DROP INDEX IF EXISTS actor.actor_usr_addr_state_idx; 
2101 DROP INDEX IF EXISTS actor.actor_usr_addr_post_code_idx;
2102
2103 CREATE INDEX actor_usr_addr_street1_idx ON actor.usr_address (evergreen.lowercase(street1));
2104 CREATE INDEX actor_usr_addr_street2_idx ON actor.usr_address (evergreen.lowercase(street2));
2105 CREATE INDEX actor_usr_addr_city_idx ON actor.usr_address (evergreen.lowercase(city));
2106 CREATE INDEX actor_usr_addr_state_idx ON actor.usr_address (evergreen.lowercase(state));
2107 CREATE INDEX actor_usr_addr_post_code_idx ON actor.usr_address (evergreen.lowercase(post_code));
2108
2109 -- update actor.usr indexes
2110 DROP INDEX IF EXISTS actor.actor_usr_first_given_name_idx;
2111 DROP INDEX IF EXISTS actor.actor_usr_second_given_name_idx;
2112 DROP INDEX IF EXISTS actor.actor_usr_family_name_idx;
2113 DROP INDEX IF EXISTS actor.actor_usr_email_idx;
2114 DROP INDEX IF EXISTS actor.actor_usr_day_phone_idx;
2115 DROP INDEX IF EXISTS actor.actor_usr_evening_phone_idx;
2116 DROP INDEX IF EXISTS actor.actor_usr_other_phone_idx;
2117 DROP INDEX IF EXISTS actor.actor_usr_ident_value_idx;
2118 DROP INDEX IF EXISTS actor.actor_usr_ident_value2_idx;
2119
2120 CREATE INDEX actor_usr_first_given_name_idx ON actor.usr (evergreen.lowercase(first_given_name));
2121 CREATE INDEX actor_usr_second_given_name_idx ON actor.usr (evergreen.lowercase(second_given_name));
2122 CREATE INDEX actor_usr_family_name_idx ON actor.usr (evergreen.lowercase(family_name));
2123 CREATE INDEX actor_usr_email_idx ON actor.usr (evergreen.lowercase(email));
2124 CREATE INDEX actor_usr_day_phone_idx ON actor.usr (evergreen.lowercase(day_phone));
2125 CREATE INDEX actor_usr_evening_phone_idx ON actor.usr (evergreen.lowercase(evening_phone));
2126 CREATE INDEX actor_usr_other_phone_idx ON actor.usr (evergreen.lowercase(other_phone));
2127 CREATE INDEX actor_usr_ident_value_idx ON actor.usr (evergreen.lowercase(ident_value));
2128 CREATE INDEX actor_usr_ident_value2_idx ON actor.usr (evergreen.lowercase(ident_value2));
2129
2130 -- update actor.card indexes
2131 DROP INDEX IF EXISTS actor.actor_card_barcode_evergreen_lowercase_idx;
2132 CREATE INDEX actor_card_barcode_evergreen_lowercase_idx ON actor.card (evergreen.lowercase(barcode));
2133
2134 CREATE OR REPLACE FUNCTION vandelay.match_bib_record ( ) RETURNS TRIGGER AS $func$
2135 DECLARE
2136     attr        RECORD;
2137     attr_def    RECORD;
2138     eg_rec      RECORD;
2139     id_value    TEXT;
2140     exact_id    BIGINT;
2141 BEGIN
2142
2143     DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
2144
2145     SELECT * INTO attr_def FROM vandelay.bib_attr_definition WHERE xpath = '//*[@tag="901"]/*[@code="c"]' ORDER BY id LIMIT 1;
2146
2147     IF attr_def IS NOT NULL AND attr_def.id IS NOT NULL THEN
2148         id_value := extract_marc_field('vandelay.queued_bib_record', NEW.id, attr_def.xpath, attr_def.remove);
2149     
2150         IF id_value IS NOT NULL AND id_value <> '' AND id_value ~ $r$^\d+$$r$ THEN
2151             SELECT id INTO exact_id FROM biblio.record_entry WHERE id = id_value::BIGINT AND NOT deleted;
2152             SELECT * INTO attr FROM vandelay.queued_bib_record_attr WHERE record = NEW.id and field = attr_def.id LIMIT 1;
2153             IF exact_id IS NOT NULL THEN
2154                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, exact_id);
2155             END IF;
2156         END IF;
2157     END IF;
2158
2159     IF exact_id IS NULL THEN
2160         FOR attr IN SELECT a.* FROM vandelay.queued_bib_record_attr a JOIN vandelay.bib_attr_definition d ON (d.id = a.field) WHERE record = NEW.id AND d.ident IS TRUE LOOP
2161     
2162                 -- All numbers? check for an id match
2163                 IF (attr.attr_value ~ $r$^\d+$$r$) THEN
2164                 FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE id = attr.attr_value::BIGINT AND deleted IS FALSE LOOP
2165                         INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
2166                         END LOOP;
2167                 END IF;
2168     
2169                 -- Looks like an ISBN? check for an isbn match
2170                 IF (attr.attr_value ~* $r$^[0-9x]+$$r$ AND character_length(attr.attr_value) IN (10,13)) THEN
2171                 FOR eg_rec IN EXECUTE $$SELECT * FROM metabib.full_rec fr WHERE fr.value LIKE evergreen.lowercase('$$ || attr.attr_value || $$%') AND fr.tag = '020' AND fr.subfield = 'a'$$ LOOP
2172                                 PERFORM id FROM biblio.record_entry WHERE id = eg_rec.record AND deleted IS FALSE;
2173                                 IF FOUND THEN
2174                                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('isbn', attr.id, NEW.id, eg_rec.record);
2175                                 END IF;
2176                         END LOOP;
2177     
2178                         -- subcheck for isbn-as-tcn
2179                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = 'i' || attr.attr_value AND deleted IS FALSE LOOP
2180                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2181                 END LOOP;
2182                 END IF;
2183     
2184                 -- check for an OCLC tcn_value match
2185                 IF (attr.attr_value ~ $r$^o\d+$$r$) THEN
2186                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = regexp_replace(attr.attr_value,'^o','ocm') AND deleted IS FALSE LOOP
2187                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2188                 END LOOP;
2189                 END IF;
2190     
2191                 -- check for a direct tcn_value match
2192             FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = attr.attr_value AND deleted IS FALSE LOOP
2193                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2194             END LOOP;
2195     
2196                 -- check for a direct item barcode match
2197             FOR eg_rec IN
2198                     SELECT  DISTINCT b.*
2199                       FROM  biblio.record_entry b
2200                             JOIN asset.call_number cn ON (cn.record = b.id)
2201                             JOIN asset.copy cp ON (cp.call_number = cn.id)
2202                       WHERE cp.barcode = attr.attr_value AND cp.deleted IS FALSE
2203             LOOP
2204                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
2205             END LOOP;
2206     
2207         END LOOP;
2208     END IF;
2209
2210     RETURN NULL;
2211 END;
2212 $func$ LANGUAGE PLPGSQL;
2213
2214
2215 -- 0499
2216 CREATE OR REPLACE FUNCTION asset.label_normalizer_generic(TEXT) RETURNS TEXT AS $func$
2217     # Created after looking at the Koha C4::ClassSortRoutine::Generic module,
2218     # thus could probably be considered a derived work, although nothing was
2219     # directly copied - but to err on the safe side of providing attribution:
2220     # Copyright (C) 2007 LibLime
2221     # Copyright (C) 2011 Equinox Software, Inc (Steve Callendar)
2222     # Licensed under the GPL v2 or later
2223
2224     use strict;
2225     use warnings;
2226
2227     # Converts the callnumber to uppercase
2228     # Strips spaces from start and end of the call number
2229     # Converts anything other than letters, digits, and periods into spaces
2230     # Collapses multiple spaces into a single underscore
2231     my $callnum = uc(shift);
2232     $callnum =~ s/^\s//g;
2233     $callnum =~ s/\s$//g;
2234     # NOTE: this previously used underscores, but this caused sorting issues
2235     # for the "before" half of page 0 on CN browse, sorting CNs containing a
2236     # decimal before "whole number" CNs
2237     $callnum =~ s/[^A-Z0-9_.]/ /g;
2238     $callnum =~ s/ {2,}/ /g;
2239
2240     return $callnum;
2241 $func$ LANGUAGE PLPERLU;
2242
2243
2244
2245 -- 0501
2246 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('language','Language (2.0 compat version)','Lang');
2247 UPDATE metabib.record_attr SET attrs = attrs || hstore('language',(attrs->'item_lang'));
2248
2249 -- 0502
2250 -- Dewey fields
2251 UPDATE asset.call_number_class
2252     SET field = '080ab,082ab,092abef'
2253     WHERE id = 2
2254 ;
2255
2256 -- LC fields
2257 UPDATE asset.call_number_class
2258     SET field = '050ab,055ab,090abef'
2259     WHERE id = 3
2260 ;
2261
2262 -- FAIR WARNING:
2263 -- Using a tool such as pgadmin to run this script may fail
2264 -- If it does, try psql command line.
2265
2266 -- Change this to FALSE to disable updating existing circs
2267 -- Otherwise will use the fine interval for the grace period
2268 \set CircGrace TRUE
2269
2270 -- 0503
2271 -- New Columns
2272
2273 ALTER TABLE config.rule_recurring_fine
2274     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '1 day';
2275
2276 ALTER TABLE action.circulation
2277     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
2278
2279 ALTER TABLE action.aged_circulation
2280     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
2281
2282 -- Remove defaults needed to stop null complaints
2283
2284 ALTER TABLE action.circulation
2285     ALTER COLUMN grace_period DROP DEFAULT;
2286
2287 ALTER TABLE action.aged_circulation
2288     ALTER COLUMN grace_period DROP DEFAULT;
2289
2290 -- Drop Views
2291
2292 DROP VIEW action.all_circulation;
2293 DROP VIEW action.open_circulation;
2294 DROP VIEW action.billable_circulations;
2295
2296 -- Replace Views
2297
2298 CREATE OR REPLACE VIEW action.all_circulation AS
2299     SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
2300         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
2301         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
2302         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
2303         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
2304         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
2305       FROM  action.aged_circulation
2306             UNION ALL
2307     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,
2308         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,
2309         cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
2310         circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
2311         circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
2312         circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
2313         circ.parent_circ
2314       FROM  action.circulation circ
2315         JOIN asset.copy cp ON (circ.target_copy = cp.id)
2316         JOIN asset.call_number cn ON (cp.call_number = cn.id)
2317         JOIN actor.usr p ON (circ.usr = p.id)
2318         LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
2319         LEFT JOIN actor.usr_address b ON (p.billing_address = a.id);
2320
2321 CREATE OR REPLACE VIEW action.open_circulation AS
2322         SELECT  *
2323           FROM  action.circulation
2324           WHERE checkin_time IS NULL
2325           ORDER BY due_date;
2326                 
2327
2328 CREATE OR REPLACE VIEW action.billable_circulations AS
2329         SELECT  *
2330           FROM  action.circulation
2331           WHERE xact_finish IS NULL;
2332
2333 -- Drop Functions that rely on types
2334
2335 DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT, BOOL);
2336 DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT);
2337 DROP FUNCTION action.item_user_renew_test(INT, BIGINT, INT);
2338
2339 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2340 DECLARE
2341     user_object             actor.usr%ROWTYPE;
2342     standing_penalty        config.standing_penalty%ROWTYPE;
2343     item_object             asset.copy%ROWTYPE;
2344     item_status_object      config.copy_status%ROWTYPE;
2345     item_location_object    asset.copy_location%ROWTYPE;
2346     result                  action.circ_matrix_test_result;
2347     circ_test               action.found_circ_matrix_matchpoint;
2348     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
2349     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
2350     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
2351     hold_ratio              action.hold_stats%ROWTYPE;
2352     penalty_type            TEXT;
2353     items_out               INT;
2354     context_org_list        INT[];
2355     done                    BOOL := FALSE;
2356 BEGIN
2357     -- Assume success unless we hit a failure condition
2358     result.success := TRUE;
2359
2360     -- Fail if the user is BARRED
2361     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
2362
2363     -- Fail if we couldn't find the user 
2364     IF user_object.id IS NULL THEN
2365         result.fail_part := 'no_user';
2366         result.success := FALSE;
2367         done := TRUE;
2368         RETURN NEXT result;
2369         RETURN;
2370     END IF;
2371
2372     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
2373
2374     -- Fail if we couldn't find the item 
2375     IF item_object.id IS NULL THEN
2376         result.fail_part := 'no_item';
2377         result.success := FALSE;
2378         done := TRUE;
2379         RETURN NEXT result;
2380         RETURN;
2381     END IF;
2382
2383     IF user_object.barred IS TRUE THEN
2384         result.fail_part := 'actor.usr.barred';
2385         result.success := FALSE;
2386         done := TRUE;
2387         RETURN NEXT result;
2388     END IF;
2389
2390     -- Fail if the item can't circulate
2391     IF item_object.circulate IS FALSE THEN
2392         result.fail_part := 'asset.copy.circulate';
2393         result.success := FALSE;
2394         done := TRUE;
2395         RETURN NEXT result;
2396     END IF;
2397
2398     -- Fail if the item isn't in a circulateable status on a non-renewal
2399     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
2400         result.fail_part := 'asset.copy.status';
2401         result.success := FALSE;
2402         done := TRUE;
2403         RETURN NEXT result;
2404     ELSIF renewal AND item_object.status <> 1 THEN
2405         result.fail_part := 'asset.copy.status';
2406         result.success := FALSE;
2407         done := TRUE;
2408         RETURN NEXT result;
2409     END IF;
2410
2411     -- Fail if the item can't circulate because of the shelving location
2412     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
2413     IF item_location_object.circulate IS FALSE THEN
2414         result.fail_part := 'asset.copy_location.circulate';
2415         result.success := FALSE;
2416         done := TRUE;
2417         RETURN NEXT result;
2418     END IF;
2419
2420     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
2421
2422     circ_matchpoint             := circ_test.matchpoint;
2423     result.matchpoint           := circ_matchpoint.id;
2424     result.circulate            := circ_matchpoint.circulate;
2425     result.duration_rule        := circ_matchpoint.duration_rule;
2426     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
2427     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
2428     result.hard_due_date        := circ_matchpoint.hard_due_date;
2429     result.renewals             := circ_matchpoint.renewals;
2430     result.grace_period         := circ_matchpoint.grace_period;
2431     result.buildrows            := circ_test.buildrows;
2432
2433     -- Fail if we couldn't find a matchpoint
2434     IF circ_test.success = false THEN
2435         result.fail_part := 'no_matchpoint';
2436         result.success := FALSE;
2437         done := TRUE;
2438         RETURN NEXT result;
2439         RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
2440     END IF;
2441
2442     -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
2443     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
2444
2445     IF renewal THEN
2446         penalty_type = '%RENEW%';
2447     ELSE
2448         penalty_type = '%CIRC%';
2449     END IF;
2450
2451     FOR standing_penalty IN
2452         SELECT  DISTINCT csp.*
2453           FROM  actor.usr_standing_penalty usp
2454                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
2455           WHERE usr = match_user
2456                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
2457                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
2458                 AND csp.block_list LIKE penalty_type LOOP
2459
2460         result.fail_part := standing_penalty.name;
2461         result.success := FALSE;
2462         done := TRUE;
2463         RETURN NEXT result;
2464     END LOOP;
2465
2466     -- Fail if the test is set to hard non-circulating
2467     IF circ_matchpoint.circulate IS FALSE THEN
2468         result.fail_part := 'config.circ_matrix_test.circulate';
2469         result.success := FALSE;
2470         done := TRUE;
2471         RETURN NEXT result;
2472     END IF;
2473
2474     -- Fail if the total copy-hold ratio is too low
2475     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
2476         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
2477         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
2478             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
2479             result.success := FALSE;
2480             done := TRUE;
2481             RETURN NEXT result;
2482         END IF;
2483     END IF;
2484
2485     -- Fail if the available copy-hold ratio is too low
2486     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
2487         IF hold_ratio.hold_count IS NULL THEN
2488             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
2489         END IF;
2490         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
2491             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
2492             result.success := FALSE;
2493             done := TRUE;
2494             RETURN NEXT result;
2495         END IF;
2496     END IF;
2497
2498     -- Fail if the user has too many items with specific circ_modifiers checked out
2499     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
2500         SELECT  INTO items_out COUNT(*)
2501           FROM  action.circulation circ
2502             JOIN asset.copy cp ON (cp.id = circ.target_copy)
2503           WHERE circ.usr = match_user
2504                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
2505             AND circ.checkin_time IS NULL
2506             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
2507             AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
2508         IF items_out >= out_by_circ_mod.items_out THEN
2509             result.fail_part := 'config.circ_matrix_circ_mod_test';
2510             result.success := FALSE;
2511             done := TRUE;
2512             RETURN NEXT result;
2513         END IF;
2514     END LOOP;
2515
2516     -- If we passed everything, return the successful matchpoint id
2517     IF NOT done THEN
2518         RETURN NEXT result;
2519     END IF;
2520
2521     RETURN;
2522 END;
2523 $func$ LANGUAGE plpgsql;
2524
2525 CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2526     SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
2527 $func$ LANGUAGE SQL;
2528
2529 CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2530     SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
2531 $func$ LANGUAGE SQL;
2532
2533 -- Update recurring fine rules
2534 UPDATE config.rule_recurring_fine SET grace_period=recurrence_interval;
2535
2536 -- Update Circulation Data
2537 -- Only update if we were told to and the circ hasn't been checked in
2538 UPDATE action.circulation SET grace_period=fine_interval WHERE :CircGrace AND (checkin_time IS NULL);
2539
2540 -- 0504
2541 CREATE TABLE biblio.monograph_part (
2542     id              SERIAL  PRIMARY KEY,
2543     record          BIGINT  NOT NULL REFERENCES biblio.record_entry (id),
2544     label           TEXT    NOT NULL,
2545     label_sortkey   TEXT    NOT NULL,
2546     CONSTRAINT record_label_unique UNIQUE (record,label)
2547 );
2548
2549 CREATE OR REPLACE FUNCTION biblio.normalize_biblio_monograph_part_sortkey () RETURNS TRIGGER AS $$
2550 BEGIN
2551     NEW.label_sortkey := REGEXP_REPLACE(
2552         evergreen.lpad_number_substrings(
2553             naco_normalize(NEW.label),
2554             '0',
2555             10
2556         ),
2557         E'\\s+',
2558         '',
2559         'g'
2560     );
2561     RETURN NEW;
2562 END;
2563 $$ LANGUAGE PLPGSQL;
2564
2565 CREATE TRIGGER norm_sort_label BEFORE INSERT OR UPDATE ON biblio.monograph_part FOR EACH ROW EXECUTE PROCEDURE biblio.normalize_biblio_monograph_part_sortkey();
2566
2567 CREATE TABLE asset.copy_part_map (
2568     id          SERIAL  PRIMARY KEY,
2569     target_copy BIGINT  NOT NULL, -- points o asset.copy
2570     part        INT     NOT NULL REFERENCES biblio.monograph_part (id) ON DELETE CASCADE
2571 );
2572 CREATE UNIQUE INDEX copy_part_map_cp_part_idx ON asset.copy_part_map (target_copy, part);
2573
2574 CREATE TABLE asset.call_number_prefix (
2575        id                      SERIAL   PRIMARY KEY,
2576        owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
2577        label               TEXT                NOT NULL, -- i18n
2578        label_sortkey   TEXT
2579 );
2580
2581 CREATE OR REPLACE FUNCTION asset.normalize_affix_sortkey () RETURNS TRIGGER AS $$
2582 BEGIN
2583     NEW.label_sortkey := REGEXP_REPLACE(
2584         evergreen.lpad_number_substrings(
2585             naco_normalize(NEW.label),
2586             '0',
2587             10
2588         ),
2589         E'\\s+',
2590         '',
2591         'g'
2592     );
2593     RETURN NEW;
2594 END;
2595 $$ LANGUAGE PLPGSQL;
2596
2597 CREATE TRIGGER prefix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_prefix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
2598 CREATE UNIQUE INDEX asset_call_number_prefix_once_per_lib ON asset.call_number_prefix (label, owning_lib);
2599 CREATE INDEX asset_call_number_prefix_sortkey_idx ON asset.call_number_prefix (label_sortkey);
2600
2601 CREATE TABLE asset.call_number_suffix (
2602        id                      SERIAL   PRIMARY KEY,
2603        owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
2604        label               TEXT                NOT NULL, -- i18n
2605        label_sortkey   TEXT
2606 );
2607 CREATE TRIGGER suffix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_suffix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
2608 CREATE UNIQUE INDEX asset_call_number_suffix_once_per_lib ON asset.call_number_suffix (label, owning_lib);
2609 CREATE INDEX asset_call_number_suffix_sortkey_idx ON asset.call_number_suffix (label_sortkey);
2610
2611 INSERT INTO asset.call_number_suffix (id, owning_lib, label) VALUES (-1, 1, '');
2612 INSERT INTO asset.call_number_prefix (id, owning_lib, label) VALUES (-1, 1, '');
2613
2614 DROP INDEX IF EXISTS asset.asset_call_number_label_once_per_lib;
2615
2616 ALTER TABLE asset.call_number
2617     ADD COLUMN prefix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_prefix(id) DEFERRABLE INITIALLY DEFERRED,
2618     ADD COLUMN suffix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_suffix(id) DEFERRABLE INITIALLY DEFERRED;
2619
2620 ALTER TABLE auditor.asset_call_number_history
2621     ADD COLUMN prefix INT NOT NULL DEFAULT -1,
2622     ADD COLUMN suffix INT NOT NULL DEFAULT -1;
2623
2624 CREATE UNIQUE INDEX asset_call_number_label_once_per_lib ON asset.call_number (record, owning_lib, label, prefix, suffix) WHERE deleted = FALSE OR deleted IS FALSE;
2625
2626 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
2627     'ui.cat.volume_copy_editor.horizontal',
2628     oils_i18n_gettext(
2629         'ui.cat.volume_copy_editor.horizontal',
2630         'GUI: Horizontal layout for Volume/Copy Creator/Editor.',
2631         'coust', 'label'),
2632     oils_i18n_gettext(
2633         'ui.cat.volume_copy_editor.horizontal',
2634         'The main entry point for this interface is in Holdings Maintenance, Actions for Selected Rows, Edit Item Attributes / Call Numbers / Replace Barcodes.  This setting changes the top and bottom panes for that interface into left and right panes.',
2635         'coust', 'description'),
2636     'bool'
2637 );
2638
2639
2640
2641 -- 0506
2642 ALTER FUNCTION actor.org_unit_descendants( INT, INT ) ROWS 1;
2643 ALTER FUNCTION actor.org_unit_descendants( INT ) ROWS 1;
2644 ALTER FUNCTION actor.org_unit_descendants_distance( INT )  ROWS 1;
2645 ALTER FUNCTION actor.org_unit_ancestors( INT )  ROWS 1;
2646 ALTER FUNCTION actor.org_unit_ancestors_distance( INT )  ROWS 1;
2647 ALTER FUNCTION actor.org_unit_full_path ( INT )  ROWS 2;
2648 ALTER FUNCTION actor.org_unit_full_path ( INT, INT ) ROWS 2;
2649 ALTER FUNCTION actor.org_unit_combined_ancestors ( INT, INT ) ROWS 1;
2650 ALTER FUNCTION actor.org_unit_common_ancestors ( INT, INT ) ROWS 1;
2651 ALTER FUNCTION actor.org_unit_ancestor_setting( TEXT, INT ) ROWS 1;
2652 ALTER FUNCTION permission.grp_ancestors ( INT ) ROWS 1;
2653 ALTER FUNCTION permission.grp_ancestors_distance( INT ) ROWS 1;
2654 ALTER FUNCTION permission.grp_descendants_distance( INT ) ROWS 1;
2655 ALTER FUNCTION permission.usr_perms ( INT ) ROWS 10;
2656 ALTER FUNCTION permission.usr_has_perm_at_nd ( INT, TEXT) ROWS 1;
2657 ALTER FUNCTION permission.usr_has_perm_at_all_nd ( INT, TEXT ) ROWS 1;
2658 ALTER FUNCTION permission.usr_has_perm_at ( INT, TEXT ) ROWS 1;
2659 ALTER FUNCTION permission.usr_has_perm_at_all ( INT, TEXT ) ROWS 1;
2660
2661
2662 DROP TRIGGER IF EXISTS facet_force_nfc_tgr ON metabib.facet_entry;
2663 CREATE TRIGGER facet_force_nfc_tgr
2664     BEFORE UPDATE OR INSERT ON metabib.facet_entry
2665     FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
2666
2667 DROP FUNCTION IF EXISTS public.force_unicode_normal_form (TEXT,TEXT);
2668 DROP FUNCTION IF EXISTS public.facet_force_nfc ();
2669
2670 DROP TRIGGER b_maintain_901 ON biblio.record_entry;
2671 DROP TRIGGER b_maintain_901 ON authority.record_entry;
2672 DROP TRIGGER b_maintain_901 ON serial.record_entry;
2673
2674 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2675 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2676 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON serial.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2677
2678 DROP FUNCTION IF EXISTS public.maintain_901 ();
2679
2680 ------ Backporting note: 2.1+ only beyond here --------
2681
2682 CREATE SCHEMA unapi;
2683
2684 CREATE TABLE unapi.bre_output_layout (
2685     name                TEXT    PRIMARY KEY,
2686     transform           TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
2687     mime_type           TEXT    NOT NULL,
2688     feed_top            TEXT    NOT NULL,
2689     holdings_element    TEXT,
2690     title_element       TEXT,
2691     description_element TEXT,
2692     creator_element     TEXT,
2693     update_ts_element   TEXT
2694 );
2695
2696 INSERT INTO unapi.bre_output_layout
2697     (name,           transform, mime_type,              holdings_element, feed_top,         title_element, description_element, creator_element, update_ts_element)
2698         VALUES
2699     ('holdings_xml', NULL,      'application/xml',      NULL,             'hxml',           NULL,          NULL,                NULL,            NULL),
2700     ('marcxml',      'marcxml', 'application/marc+xml', 'record',         'collection',     NULL,          NULL,                NULL,            NULL),
2701     ('mods32',       'mods32',  'application/mods+xml', 'mods',           'modsCollection', NULL,          NULL,                NULL,            NULL)
2702 ;
2703
2704 -- Dummy functions, so we can create the real ones out of order
2705 CREATE OR REPLACE FUNCTION unapi.aou    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2706 CREATE OR REPLACE FUNCTION unapi.acnp   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2707 CREATE OR REPLACE FUNCTION unapi.acns   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2708 CREATE OR REPLACE FUNCTION unapi.acn    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2709 CREATE OR REPLACE FUNCTION unapi.ssub   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2710 CREATE OR REPLACE FUNCTION unapi.sdist  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2711 CREATE OR REPLACE FUNCTION unapi.sstr   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2712 CREATE OR REPLACE FUNCTION unapi.sitem  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2713 CREATE OR REPLACE FUNCTION unapi.sunit  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2714 CREATE OR REPLACE FUNCTION unapi.sisum  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2715 CREATE OR REPLACE FUNCTION unapi.sbsum  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2716 CREATE OR REPLACE FUNCTION unapi.sssum  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2717 CREATE OR REPLACE FUNCTION unapi.siss   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2718 CREATE OR REPLACE FUNCTION unapi.auri   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2719 CREATE OR REPLACE FUNCTION unapi.acp    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2720 CREATE OR REPLACE FUNCTION unapi.acpn   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2721 CREATE OR REPLACE FUNCTION unapi.acl    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2722 CREATE OR REPLACE FUNCTION unapi.ccs    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2723 CREATE OR REPLACE FUNCTION unapi.ascecm ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2724 CREATE OR REPLACE FUNCTION unapi.bre    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2725 CREATE OR REPLACE FUNCTION unapi.bmp    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2726
2727 CREATE OR REPLACE FUNCTION unapi.holdings_xml ( bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2728 CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2729
2730 CREATE OR REPLACE FUNCTION unapi.memoize (classname TEXT, obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2731 DECLARE
2732     key     TEXT;
2733     output  XML;
2734 BEGIN
2735     key :=
2736         'id'        || COALESCE(obj_id::TEXT,'') ||
2737         'format'    || COALESCE(format::TEXT,'') ||
2738         'ename'     || COALESCE(ename::TEXT,'') ||
2739         'includes'  || COALESCE(includes::TEXT,'{}'::TEXT[]::TEXT) ||
2740         'org'       || COALESCE(org::TEXT,'') ||
2741         'depth'     || COALESCE(depth::TEXT,'') ||
2742         'slimit'    || COALESCE(slimit::TEXT,'') ||
2743         'soffset'   || COALESCE(soffset::TEXT,'') ||
2744         'include_xmlns'   || COALESCE(include_xmlns::TEXT,'');
2745     -- RAISE NOTICE 'memoize key: %', key;
2746
2747     key := MD5(key);
2748     -- RAISE NOTICE 'memoize hash: %', key;
2749
2750     -- XXX cache logic ... memcached? table?
2751
2752     EXECUTE $$SELECT unapi.$$ || classname || $$( $1, $2, $3, $4, $5, $6, $7, $8, $9);$$ INTO output USING obj_id, format, ename, includes, org, depth, slimit, soffset, include_xmlns;
2753     RETURN output;
2754 END;
2755 $F$ LANGUAGE PLPGSQL;
2756
2757 CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$
2758 DECLARE
2759     layout          unapi.bre_output_layout%ROWTYPE;
2760     transform       config.xml_transform%ROWTYPE;
2761     item_format     TEXT;
2762     tmp_xml         TEXT;
2763     xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
2764     ouid            INT;
2765     element_list    TEXT[];
2766 BEGIN
2767
2768     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
2769     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
2770
2771     IF layout.name IS NULL THEN
2772         RETURN NULL::XML;
2773     END IF;
2774
2775     SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
2776     xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
2777
2778     -- Gather the bib xml
2779     SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
2780
2781     IF layout.title_element IS NOT NULL THEN
2782         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title, include_xmlns;
2783     END IF;
2784
2785     IF layout.description_element IS NOT NULL THEN
2786         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description, include_xmlns;
2787     END IF;
2788
2789     IF layout.creator_element IS NOT NULL THEN
2790         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator, include_xmlns;
2791     END IF;
2792
2793     IF layout.update_ts_element IS NOT NULL THEN
2794         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts, include_xmlns;
2795     END IF;
2796
2797     IF unapi_url IS NOT NULL THEN
2798         EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
2799     END IF;
2800
2801     IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
2802
2803     element_list := regexp_split_to_array(layout.feed_top,E'\\.');
2804     FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
2805         EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, include_xmlns;
2806     END LOOP;
2807
2808     RETURN tmp_xml::XML;
2809 END;
2810 $F$ LANGUAGE PLPGSQL;
2811
2812 CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2813 DECLARE
2814     me      biblio.record_entry%ROWTYPE;
2815     layout  unapi.bre_output_layout%ROWTYPE;
2816     xfrm    config.xml_transform%ROWTYPE;
2817     ouid    INT;
2818     tmp_xml TEXT;
2819     top_el  TEXT;
2820     output  XML;
2821     hxml    XML;
2822 BEGIN
2823
2824     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
2825
2826     IF ouid IS NULL THEN
2827         RETURN NULL::XML;
2828     END IF;
2829
2830     IF format = 'holdings_xml' THEN -- the special case
2831         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
2832         RETURN output;
2833     END IF;
2834
2835     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
2836
2837     IF layout.name IS NULL THEN
2838         RETURN NULL::XML;
2839     END IF;
2840
2841     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
2842
2843     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
2844
2845     -- grab hodlings if we need them
2846     IF ('holdings_xml' = ANY (includes)) THEN 
2847         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
2848     ELSE
2849         hxml := NULL::XML;
2850     END IF;
2851
2852
2853     -- generate our item node
2854
2855
2856     IF format = 'marcxml' THEN
2857         tmp_xml := me.marc;
2858         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
2859            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
2860         END IF; 
2861     ELSE
2862         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
2863     END IF;
2864
2865     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
2866
2867     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
2868         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
2869     END IF;
2870
2871     IF ('bre.unapi' = ANY (includes)) THEN 
2872         output := REGEXP_REPLACE(
2873             tmp_xml,
2874             '</' || top_el || '>(.*?)',
2875             XMLELEMENT(
2876                 name abbr,
2877                 XMLATTRIBUTES(
2878                     'http://www.w3.org/1999/xhtml' AS xmlns,
2879                     'unapi-id' AS class,
2880                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
2881                 )
2882             )::TEXT || '</' || top_el || E'>\\1'
2883         );
2884     ELSE
2885         output := tmp_xml;
2886     END IF;
2887
2888     RETURN output;
2889 END;
2890 $F$ LANGUAGE PLPGSQL;
2891
2892 CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$
2893      SELECT  XMLELEMENT(
2894                  name holdings,
2895                  XMLATTRIBUTES(
2896                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2897                     CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
2898                  ),
2899                  XMLELEMENT(
2900                      name counts,
2901                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
2902                          SELECT  XMLELEMENT(
2903                                      name count,
2904                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2905                                  )::text
2906                            FROM  asset.opac_ou_record_copy_count($2,  $1)
2907                                      UNION
2908                          SELECT  XMLELEMENT(
2909                                      name count,
2910                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2911                                  )::text
2912                            FROM  asset.staff_ou_record_copy_count($2, $1)
2913                                      ORDER BY 1
2914                      )x)
2915                  ),
2916                  CASE 
2917                      WHEN ('bmp' = ANY ($5)) THEN
2918                         XMLELEMENT( name monograph_parts,
2919                             XMLAGG((SELECT unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE) FROM biblio.monograph_part WHERE record = $1))
2920                         )
2921                      ELSE NULL
2922                  END,
2923                  CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
2924                      XMLELEMENT(
2925                          name volumes,
2926                          (SELECT XMLAGG(acn) FROM (
2927                             SELECT  unapi.acn(acn.id,'xml','volume', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value('{acn,auri}'::TEXT[] || $5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
2928                               FROM  asset.call_number acn
2929                               WHERE acn.record = $1
2930                                     AND EXISTS (
2931                                         SELECT  1
2932                                           FROM  asset.copy acp
2933                                                 JOIN actor.org_unit_descendants(
2934                                                     $2,
2935                                                     (COALESCE(
2936                                                         $4,
2937                                                         (SELECT aout.depth
2938                                                           FROM  actor.org_unit_type aout
2939                                                                 JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
2940                                                         )
2941                                                     ))
2942                                                 ) aoud ON (acp.circ_lib = aoud.id)
2943                                           LIMIT 1
2944                                     )
2945                               ORDER BY label_sortkey
2946                               LIMIT $6
2947                               OFFSET $7
2948                          )x)
2949                      )
2950                  ELSE NULL END,
2951                  CASE WHEN ('ssub' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
2952                      XMLELEMENT(
2953                          name subscriptions,
2954                          (SELECT XMLAGG(ssub) FROM (
2955                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
2956                               FROM  serial.subscription
2957                               WHERE record_entry = $1
2958                         )x)
2959                      )
2960                  ELSE NULL END
2961              );
2962 $F$ LANGUAGE SQL;
2963
2964 CREATE OR REPLACE FUNCTION unapi.ssub ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2965         SELECT  XMLELEMENT(
2966                     name subscription,
2967                     XMLATTRIBUTES(
2968                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2969                         'tag:open-ils.org:U2@ssub/' || id AS id,
2970                         start_date AS start, end_date AS end, expected_date_offset
2971                     ),
2972                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8),
2973                     XMLELEMENT( name distributions,
2974                         CASE 
2975                             WHEN ('sdist' = ANY ($4)) THEN
2976                                 XMLAGG((SELECT unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE) FROM serial.distribution WHERE subscription = ssub.id))
2977                             ELSE NULL
2978                         END
2979                     )
2980                 )
2981           FROM  serial.subscription ssub
2982           WHERE id = $1
2983           GROUP BY id, start_date, end_date, expected_date_offset, owning_lib;
2984 $F$ LANGUAGE SQL;
2985
2986 CREATE OR REPLACE FUNCTION unapi.sdist ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2987         SELECT  XMLELEMENT(
2988                     name distribution,
2989                     XMLATTRIBUTES(
2990                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2991                         'tag:open-ils.org:U2@sdist/' || id AS id,
2992                         'tag:open-ils.org:U2@acn/' || receive_call_number AS receive_call_number,
2993                         'tag:open-ils.org:U2@acn/' || bind_call_number AS bind_call_number,
2994                         unit_label_prefix, label, unit_label_suffix, summary_method
2995                     ),
2996                     unapi.aou( holding_lib, $2, 'holding_lib', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8),
2997                     CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) ELSE NULL END,
2998                     XMLELEMENT( name streams,
2999                         CASE 
3000                             WHEN ('sstr' = ANY ($4)) THEN
3001                                 XMLAGG((SELECT unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.stream WHERE distribution = sdist.id))
3002                             ELSE NULL
3003                         END
3004                     ),
3005                     XMLELEMENT( name summaries,
3006                         CASE 
3007                             WHEN ('ssum' = ANY ($4)) THEN
3008                                 XMLAGG((SELECT unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.basic_summary WHERE distribution = sdist.id))
3009                             ELSE NULL
3010                         END,
3011                         CASE 
3012                             WHEN ('ssum' = ANY ($4)) THEN
3013                                 XMLAGG((SELECT unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.index_summary WHERE distribution = sdist.id))
3014                             ELSE NULL
3015                         END,
3016                         CASE 
3017                             WHEN ('ssum' = ANY ($4)) THEN
3018                                 XMLAGG((SELECT unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.supplement_summary WHERE distribution = sdist.id))
3019                             ELSE NULL
3020                         END
3021                     )
3022                 )
3023           FROM  serial.distribution sdist
3024           WHERE id = $1
3025           GROUP BY id, label, unit_label_prefix, unit_label_suffix, holding_lib, summary_method, subscription, receive_call_number, bind_call_number;
3026 $F$ LANGUAGE SQL;
3027
3028 CREATE OR REPLACE FUNCTION unapi.sstr ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3029     SELECT  XMLELEMENT(
3030                 name stream,
3031                 XMLATTRIBUTES(
3032                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3033                     'tag:open-ils.org:U2@sstr/' || id AS id,
3034                     routing_label
3035                 ),
3036                 CASE WHEN distribution IS NOT NULL AND ('sdist' = ANY ($4)) THEN unapi.sssum( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3037                 XMLELEMENT( name items,
3038                     CASE 
3039                         WHEN ('sitem' = ANY ($4)) THEN
3040                             XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE stream = sstr.id))
3041                         ELSE NULL
3042                     END
3043                 )
3044             )
3045       FROM  serial.stream sstr
3046       WHERE id = $1
3047       GROUP BY id, routing_label, distribution;
3048 $F$ LANGUAGE SQL;
3049
3050 CREATE OR REPLACE FUNCTION unapi.siss ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3051     SELECT  XMLELEMENT(
3052                 name issuance,
3053                 XMLATTRIBUTES(
3054                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3055                     'tag:open-ils.org:U2@siss/' || id AS id,
3056                     create_date, edit_date, label, date_published,
3057                     holding_code, holding_type, holding_link_id
3058                 ),
3059                 CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3060                 XMLELEMENT( name items,
3061                     CASE 
3062                         WHEN ('sitem' = ANY ($4)) THEN
3063                             XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE issuance = sstr.id))
3064                         ELSE NULL
3065                     END
3066                 )
3067             )
3068       FROM  serial.issuance sstr
3069       WHERE id = $1
3070       GROUP BY id, create_date, edit_date, label, date_published, holding_code, holding_type, holding_link_id, subscription;
3071 $F$ LANGUAGE SQL;
3072
3073 CREATE OR REPLACE FUNCTION unapi.sitem ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3074         SELECT  XMLELEMENT(
3075                     name serial_item,
3076                     XMLATTRIBUTES(
3077                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3078                         'tag:open-ils.org:U2@sitem/' || id AS id,
3079                         'tag:open-ils.org:U2@siss/' || issuance AS issuance,
3080                         date_expected, date_received
3081                     ),
3082                     CASE WHEN issuance IS NOT NULL AND ('siss' = ANY ($4)) THEN unapi.siss( issuance, $2, 'issuance', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3083                     CASE WHEN stream IS NOT NULL AND ('sstr' = ANY ($4)) THEN unapi.sstr( stream, $2, 'stream', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3084                     CASE WHEN unit IS NOT NULL AND ('sunit' = ANY ($4)) THEN unapi.sunit( stream, $2, 'serial_unit', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3085                     CASE WHEN uri IS NOT NULL AND ('auri' = ANY ($4)) THEN unapi.auri( uri, $2, 'uri', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END
3086 --                    XMLELEMENT( name notes,
3087 --                        CASE 
3088 --                            WHEN ('acpn' = ANY ($4)) THEN
3089 --                                XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
3090 --                            ELSE NULL
3091 --                        END
3092 --                    )
3093                 )
3094           FROM  serial.item sitem
3095           WHERE id = $1;
3096 $F$ LANGUAGE SQL;
3097
3098
3099 CREATE OR REPLACE FUNCTION unapi.sssum ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3100     SELECT  XMLELEMENT(
3101                 name serial_summary,
3102                 XMLATTRIBUTES(
3103                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3104                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3105                     'sssum' AS type, generated_coverage, textual_holdings, show_generated
3106                 ),
3107                 CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
3108             )
3109       FROM  serial.supplement_summary ssum
3110       WHERE id = $1
3111       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3112 $F$ LANGUAGE SQL;
3113
3114 CREATE OR REPLACE FUNCTION unapi.sbsum ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3115     SELECT  XMLELEMENT(
3116                 name serial_summary,
3117                 XMLATTRIBUTES(
3118                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3119                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3120                     'sbsum' AS type, generated_coverage, textual_holdings, show_generated
3121                 ),
3122                 CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
3123             )
3124       FROM  serial.basic_summary ssum
3125       WHERE id = $1
3126       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3127 $F$ LANGUAGE SQL;
3128
3129 CREATE OR REPLACE FUNCTION unapi.sisum ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3130     SELECT  XMLELEMENT(
3131                 name serial_summary,
3132                 XMLATTRIBUTES(
3133                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3134                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3135                     'sisum' AS type, generated_coverage, textual_holdings, show_generated
3136                 ),
3137                 CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
3138             )
3139       FROM  serial.index_summary ssum
3140       WHERE id = $1
3141       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3142 $F$ LANGUAGE SQL;
3143
3144
3145 CREATE OR REPLACE FUNCTION unapi.aou ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3146 DECLARE
3147     output XML;
3148 BEGIN
3149     IF ename = 'circlib' THEN
3150         SELECT  XMLELEMENT(
3151                     name circlib,
3152                     XMLATTRIBUTES(
3153                         'http://open-ils.org/spec/actors/v1' AS xmlns,
3154                         id AS ident
3155                     ),
3156                     name
3157                 ) INTO output
3158           FROM  actor.org_unit aou
3159           WHERE id = obj_id;
3160     ELSE
3161         EXECUTE $$SELECT  XMLELEMENT(
3162                     name $$ || ename || $$,
3163                     XMLATTRIBUTES(
3164                         'http://open-ils.org/spec/actors/v1' AS xmlns,
3165                         'tag:open-ils.org:U2@aou/' || id AS id,
3166                         shortname, name, opac_visible
3167                     )
3168                 )
3169           FROM  actor.org_unit aou
3170          WHERE id = $1 $$ INTO output USING obj_id;
3171     END IF;
3172
3173     RETURN output;
3174
3175 END;
3176 $F$ LANGUAGE PLPGSQL;
3177
3178 CREATE OR REPLACE FUNCTION unapi.acl ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3179     SELECT  XMLELEMENT(
3180                 name location,
3181                 XMLATTRIBUTES(
3182                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3183                     id AS ident
3184                 ),
3185                 name
3186             )
3187       FROM  asset.copy_location
3188       WHERE id = $1;
3189 $F$ LANGUAGE SQL;
3190
3191 CREATE OR REPLACE FUNCTION unapi.ccs ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3192     SELECT  XMLELEMENT(
3193                 name status,
3194                 XMLATTRIBUTES(
3195                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3196                     id AS ident
3197                 ),
3198                 name
3199             )
3200       FROM  config.copy_status
3201       WHERE id = $1;
3202 $F$ LANGUAGE SQL;
3203
3204 CREATE OR REPLACE FUNCTION unapi.acpn ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3205         SELECT  XMLELEMENT(
3206                     name copy_note,
3207                     XMLATTRIBUTES(
3208                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3209                         create_date AS date,
3210                         title
3211                     ),
3212                     value
3213                 )
3214           FROM  asset.copy_note
3215           WHERE id = $1;
3216 $F$ LANGUAGE SQL;
3217
3218 CREATE OR REPLACE FUNCTION unapi.ascecm ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3219         SELECT  XMLELEMENT(
3220                     name statcat,
3221                     XMLATTRIBUTES(
3222                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3223                         sc.name,
3224                         sc.opac_visible
3225                     ),
3226                     asce.value
3227                 )
3228           FROM  asset.stat_cat_entry asce
3229                 JOIN asset.stat_cat sc ON (sc.id = asce.stat_cat)
3230           WHERE asce.id = $1;
3231 $F$ LANGUAGE SQL;
3232
3233 CREATE OR REPLACE FUNCTION unapi.bmp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3234         SELECT  XMLELEMENT(
3235                     name monograph_part,
3236                     XMLATTRIBUTES(
3237                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3238                         'tag:open-ils.org:U2@bmp/' || id AS id,
3239                         id AS ident,
3240                         label,
3241                         label_sortkey,
3242                         'tag:open-ils.org:U2@bre/' || record AS record
3243                     ),
3244                     CASE 
3245                         WHEN ('acp' = ANY ($4)) THEN
3246                             XMLELEMENT( name copies,
3247                                 (SELECT XMLAGG(acp) FROM (
3248                                     SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE)
3249                                       FROM  asset.copy cp
3250                                             JOIN asset.copy_part_map cpm ON (cpm.target_copy = cp.id)
3251                                       WHERE cpm.part = $1
3252                                       ORDER BY COALESCE(cp.copy_number,0), cp.barcode
3253                                       LIMIT $7
3254                                       OFFSET $8
3255                                 )x)
3256                             )
3257                         ELSE NULL
3258                     END,
3259                     CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( record, 'marcxml', 'record', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE) ELSE NULL END
3260                 )
3261           FROM  biblio.monograph_part
3262           WHERE id = $1
3263           GROUP BY id, label, label_sortkey, record;
3264 $F$ LANGUAGE SQL;
3265
3266 CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3267         SELECT  XMLELEMENT(
3268                     name copy,
3269                     XMLATTRIBUTES(
3270                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3271                         'tag:open-ils.org:U2@acp/' || id AS id,
3272                         create_date, edit_date, copy_number, circulate, deposit,
3273                         ref, holdable, deleted, deposit_amount, price, barcode,
3274                         circ_modifier, circ_as_type, opac_visible
3275                     ),
3276                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3277                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3278                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3279                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3280                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3281                     XMLELEMENT( name copy_notes,
3282                         CASE 
3283                             WHEN ('acpn' = ANY ($4)) THEN
3284                                 XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
3285                             ELSE NULL
3286                         END
3287                     ),
3288                     XMLELEMENT( name statcats,
3289                         CASE 
3290                             WHEN ('ascecm' = ANY ($4)) THEN
3291                                 XMLAGG((SELECT unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
3292                             ELSE NULL
3293                         END
3294                     ),
3295                     CASE 
3296                         WHEN ('bmp' = ANY ($4)) THEN
3297                             XMLELEMENT( name monograph_parts,
3298                                 XMLAGG((SELECT unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_part_map WHERE target_copy = cp.id))
3299                             )
3300                         ELSE NULL
3301                     END
3302                 )
3303           FROM  asset.copy cp
3304           WHERE id = $1
3305           GROUP BY id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible;
3306 $F$ LANGUAGE SQL;
3307
3308 CREATE OR REPLACE FUNCTION unapi.sunit ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3309         SELECT  XMLELEMENT(
3310                     name serial_unit,
3311                     XMLATTRIBUTES(
3312                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3313                         'tag:open-ils.org:U2@acp/' || id AS id,
3314                         create_date, edit_date, copy_number, circulate, deposit,
3315                         ref, holdable, deleted, deposit_amount, price, barcode,
3316                         circ_modifier, circ_as_type, opac_visible, status_changed_time,
3317                         floating, mint_condition, detailed_contents, sort_key, summary_contents, cost 
3318                     ),
3319                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
3320                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
3321                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
3322                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
3323                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3324                     XMLELEMENT( name copy_notes,
3325                         CASE 
3326                             WHEN ('acpn' = ANY ($4)) THEN
3327                                 XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
3328                             ELSE NULL
3329                         END
3330                     ),
3331                     XMLELEMENT( name statcats,
3332                         CASE 
3333                             WHEN ('ascecm' = ANY ($4)) THEN
3334                                 XMLAGG((SELECT unapi.acpn( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
3335                             ELSE NULL
3336                         END
3337                     )
3338                 )
3339           FROM  serial.unit cp
3340           WHERE id = $1
3341           GROUP BY  id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, floating, mint_condition,
3342                     deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible, status_changed_time, detailed_contents, sort_key, summary_contents, cost;
3343 $F$ LANGUAGE SQL;
3344
3345 CREATE OR REPLACE FUNCTION unapi.acn ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3346         SELECT  XMLELEMENT(
3347                     name volume,
3348                     XMLATTRIBUTES(
3349                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3350                         'tag:open-ils.org:U2@acn/' || acn.id AS id,
3351                         o.shortname AS lib,
3352                         o.opac_visible AS opac_visible,
3353                         deleted, label, label_sortkey, label_class, record
3354                     ),
3355                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8),
3356                     XMLELEMENT( name copies,
3357                         CASE 
3358                             WHEN ('acp' = ANY ($4)) THEN
3359                                 (SELECT XMLAGG(acp) FROM (
3360                                     SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE)
3361                                       FROM  asset.copy cp
3362                                             JOIN actor.org_unit_descendants(
3363                                                 (SELECT id FROM actor.org_unit WHERE shortname = $5),
3364                                                 (COALESCE($6,(SELECT aout.depth FROM actor.org_unit_type aout JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.shortname = $5))))
3365                                             ) aoud ON (cp.circ_lib = aoud.id)
3366                                       WHERE cp.call_number = acn.id
3367                                       ORDER BY COALESCE(cp.copy_number,0), cp.barcode
3368                                       LIMIT $7
3369                                       OFFSET $8
3370                                 )x)
3371                             ELSE NULL
3372                         END
3373                     ),
3374                     XMLELEMENT(
3375                         name uris,
3376                         (SELECT XMLAGG(auri) FROM (SELECT unapi.auri(uri,'xml','uri', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) FROM asset.uri_call_number_map WHERE call_number = acn.id)x)
3377                     ),
3378                     CASE WHEN ('acnp' = ANY ($4)) THEN unapi.acnp( acn.prefix, 'marcxml', 'prefix', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3379                     CASE WHEN ('acns' = ANY ($4)) THEN unapi.acns( acn.suffix, 'marcxml', 'suffix', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3380                     CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( acn.record, 'marcxml', 'record', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) ELSE NULL END
3381                 ) AS x
3382           FROM  asset.call_number acn
3383                 JOIN actor.org_unit o ON (o.id = acn.owning_lib)
3384           WHERE acn.id = $1
3385           GROUP BY acn.id, o.shortname, o.opac_visible, deleted, label, label_sortkey, label_class, owning_lib, record, acn.prefix, acn.suffix;
3386 $F$ LANGUAGE SQL;
3387
3388 CREATE OR REPLACE FUNCTION unapi.acnp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3389         SELECT  XMLELEMENT(
3390                     name call_number_prefix,
3391                     XMLATTRIBUTES(
3392                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3393                         id AS ident,
3394                         label,
3395                         label_sortkey
3396                     ),
3397                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acnp'), $5, $6, $7, $8)
3398                 )
3399           FROM  asset.call_number_prefix
3400           WHERE id = $1;
3401 $F$ LANGUAGE SQL;
3402
3403 CREATE OR REPLACE FUNCTION unapi.acns ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3404         SELECT  XMLELEMENT(
3405                     name call_number_suffix,
3406                     XMLATTRIBUTES(
3407                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3408                         id AS ident,
3409                         label,
3410                         label_sortkey
3411                     ),
3412                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acns'), $5, $6, $7, $8)
3413                 )
3414           FROM  asset.call_number_suffix
3415           WHERE id = $1;
3416 $F$ LANGUAGE SQL;
3417
3418 CREATE OR REPLACE FUNCTION unapi.auri ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3419         SELECT  XMLELEMENT(
3420                     name volume,
3421                     XMLATTRIBUTES(
3422                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3423                         'tag:open-ils.org:U2@auri/' || uri.id AS id,
3424                         use_restriction,
3425                         href,
3426                         label
3427                     ),
3428                     XMLELEMENT( name copies,
3429                         CASE 
3430                             WHEN ('acn' = ANY ($4)) THEN
3431                                 (SELECT XMLAGG(acn) FROM (SELECT unapi.acn( call_number, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'auri'), $5, $6, $7, $8, FALSE) FROM asset.uri_call_number_map WHERE uri = uri.id)x)
3432                             ELSE NULL
3433                         END
3434                     )
3435                 ) AS x
3436           FROM  asset.uri uri
3437           WHERE uri.id = $1
3438           GROUP BY uri.id, use_restriction, href, label;
3439 $F$ LANGUAGE SQL;
3440
3441 DROP FUNCTION IF EXISTS public.array_remove_item_by_value(ANYARRAY,ANYELEMENT);
3442
3443 DROP FUNCTION IF EXISTS public.lpad_number_substrings(TEXT,TEXT,INT);
3444
3445
3446 -- 0511
3447 CREATE OR REPLACE FUNCTION evergreen.fake_fkey_tgr () RETURNS TRIGGER AS $F$
3448 DECLARE
3449     copy_id BIGINT;
3450 BEGIN
3451     EXECUTE 'SELECT ($1).' || quote_ident(TG_ARGV[0]) INTO copy_id USING NEW;
3452     PERFORM * FROM asset.copy WHERE id = copy_id;
3453     IF NOT FOUND THEN
3454         RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
3455     END IF;
3456     RETURN NULL;
3457 END;
3458 $F$ LANGUAGE PLPGSQL;
3459
3460 DROP TRIGGER IF EXISTS action_circulation_target_copy_trig ON action.circulation;
3461 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');
3462
3463 -- 0512
3464 CREATE TABLE biblio.peer_type (
3465     id      SERIAL  PRIMARY KEY,
3466     name        TEXT        NOT NULL UNIQUE -- i18n
3467 );
3468
3469 CREATE TABLE biblio.peer_bib_copy_map (
3470     id      SERIAL  PRIMARY KEY,
3471     peer_type   INT     NOT NULL REFERENCES biblio.peer_type (id),
3472     peer_record BIGINT      NOT NULL REFERENCES biblio.record_entry (id),
3473     target_copy BIGINT      NOT NULL -- can't use fkey because of acp subtables
3474 );
3475 CREATE INDEX peer_bib_copy_map_record_idx ON biblio.peer_bib_copy_map (peer_record);
3476 CREATE INDEX peer_bib_copy_map_copy_idx ON biblio.peer_bib_copy_map (target_copy);
3477
3478 DROP TABLE asset.opac_visible_copies;
3479 CREATE TABLE asset.opac_visible_copies (
3480   id        BIGSERIAL primary key,
3481   copy_id   BIGINT,
3482   record    BIGINT,
3483   circ_lib  INTEGER
3484 );
3485
3486 INSERT INTO biblio.peer_type (id,name) VALUES
3487     (1,oils_i18n_gettext(1,'Bound Volume','bpt','name')),
3488     (2,oils_i18n_gettext(2,'Bilingual','bpt','name')),
3489     (3,oils_i18n_gettext(3,'Back-to-back','bpt','name')),
3490     (4,oils_i18n_gettext(4,'Set','bpt','name')),
3491     (5,oils_i18n_gettext(5,'e-Reader Preload','bpt','name')); 
3492
3493 SELECT SETVAL('biblio.peer_type_id_seq'::TEXT, 100);
3494
3495 CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$
3496      SELECT  XMLELEMENT(
3497                  name holdings,
3498                  XMLATTRIBUTES(
3499                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3500                     CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
3501                  ),
3502                  XMLELEMENT(
3503                      name counts,
3504                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
3505                          SELECT  XMLELEMENT(
3506                                      name count,
3507                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
3508                                  )::text
3509                            FROM  asset.opac_ou_record_copy_count($2,  $1)
3510                                      UNION
3511                          SELECT  XMLELEMENT(
3512                                      name count,
3513                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
3514                                  )::text
3515                            FROM  asset.staff_ou_record_copy_count($2, $1)
3516                                      ORDER BY 1
3517                      )x)
3518                  ),
3519                  CASE
3520                      WHEN ('bmp' = ANY ($5)) THEN
3521                         XMLELEMENT( name monograph_parts,
3522                             XMLAGG((SELECT unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE) FROM biblio.monograph_part WHERE record = $1))
3523                         )
3524                      ELSE NULL
3525                  END,
3526                  CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN
3527                      XMLELEMENT(
3528                          name volumes,
3529                          (SELECT XMLAGG(acn) FROM (
3530                             SELECT  unapi.acn(acn.id,'xml','volume',evergreen.array_remove_item_by_value(evergreen.array_remove_item_by_value('{acn,auri}'::TEXT[] || $5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
3531                               FROM  asset.call_number acn
3532                               WHERE acn.record = $1
3533                                     AND EXISTS (
3534                                         SELECT  1
3535                                           FROM  asset.copy acp
3536                                                 JOIN actor.org_unit_descendants(
3537                                                     $2,
3538                                                     (COALESCE(
3539                                                         $4,
3540                                                         (SELECT aout.depth
3541                                                           FROM  actor.org_unit_type aout
3542                                                                 JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
3543                                                         )
3544                                                     ))
3545                                                 ) aoud ON (acp.circ_lib = aoud.id)
3546                                           LIMIT 1
3547                                     )
3548                               ORDER BY label_sortkey
3549                               LIMIT $6
3550                               OFFSET $7
3551                          )x)
3552                      )
3553                  ELSE NULL END,
3554                  CASE WHEN ('ssub' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN
3555                      XMLELEMENT(
3556                          name subscriptions,
3557                          (SELECT XMLAGG(ssub) FROM (
3558                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
3559                               FROM  serial.subscription
3560                               WHERE record_entry = $1
3561                         )x)
3562                      )
3563                  ELSE NULL END,
3564                  CASE WHEN ('acp' = ANY ($5)) THEN
3565                      XMLELEMENT(
3566                          name foreign_copies,
3567                          (SELECT XMLAGG(acp) FROM (
3568                             SELECT  unapi.acp(p.target_copy,'xml','copy','{}'::TEXT[], $3, $4, $6, $7, FALSE)
3569                               FROM  biblio.peer_bib_copy_map p
3570                                     JOIN asset.copy c ON (p.target_copy = c.id)
3571                               WHERE NOT c.deleted AND peer_record = $1
3572                         )x)
3573                      )
3574                  ELSE NULL END
3575              );
3576 $F$ LANGUAGE SQL;
3577
3578 CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3579         SELECT  XMLELEMENT(
3580                     name copy,
3581                     XMLATTRIBUTES(
3582                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3583                         'tag:open-ils.org:U2@acp/' || id AS id,
3584                         create_date, edit_date, copy_number, circulate, deposit,
3585                         ref, holdable, deleted, deposit_amount, price, barcode,
3586                         circ_modifier, circ_as_type, opac_visible
3587                     ),
3588                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3589                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3590                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3591                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3592                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3593                     XMLELEMENT( name copy_notes,
3594                         CASE
3595                             WHEN ('acpn' = ANY ($4)) THEN
3596                                 XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
3597                             ELSE NULL
3598                         END
3599                     ),
3600                     XMLELEMENT( name statcats,
3601                         CASE
3602                             WHEN ('ascecm' = ANY ($4)) THEN
3603                                 XMLAGG((SELECT unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
3604                             ELSE NULL
3605                         END
3606                     ),
3607                     XMLELEMENT( name foreign_records,
3608                         CASE
3609                             WHEN ('bre' = ANY ($4)) THEN
3610                                 XMLAGG((SELECT unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE) FROM biblio.peer_bib_copy_map WHERE target_copy = cp.id))
3611                             ELSE NULL
3612                         END
3613
3614                     ),
3615                     CASE
3616                         WHEN ('bmp' = ANY ($4)) THEN
3617                             XMLELEMENT( name monograph_parts,
3618                                 XMLAGG((SELECT unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_part_map WHERE target_copy = cp.id))
3619                             )
3620                         ELSE NULL
3621                     END
3622                 )
3623           FROM  asset.copy cp
3624           WHERE id = $1
3625           GROUP BY id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible;
3626 $F$ LANGUAGE SQL;
3627
3628 CREATE OR REPLACE FUNCTION asset.refresh_opac_visible_copies_mat_view () RETURNS VOID AS $$
3629
3630     TRUNCATE TABLE asset.opac_visible_copies;
3631
3632     INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
3633     SELECT  cp.id, cp.circ_lib, cn.record
3634     FROM  asset.copy cp
3635         JOIN asset.call_number cn ON (cn.id = cp.call_number)
3636         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3637         JOIN asset.copy_location cl ON (cp.location = cl.id)
3638         JOIN config.copy_status cs ON (cp.status = cs.id)
3639         JOIN biblio.record_entry b ON (cn.record = b.id)
3640     WHERE NOT cp.deleted
3641         AND NOT cn.deleted
3642         AND NOT b.deleted
3643         AND cs.opac_visible
3644         AND cl.opac_visible
3645         AND cp.opac_visible
3646         AND a.opac_visible
3647             UNION
3648     SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record
3649     FROM  asset.copy cp
3650         JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
3651         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3652         JOIN asset.copy_location cl ON (cp.location = cl.id)
3653         JOIN config.copy_status cs ON (cp.status = cs.id)
3654     WHERE NOT cp.deleted
3655         AND cs.opac_visible
3656         AND cl.opac_visible
3657         AND cp.opac_visible
3658         AND a.opac_visible;
3659
3660 $$ LANGUAGE SQL;
3661 COMMENT ON FUNCTION asset.refresh_opac_visible_copies_mat_view() IS $$
3662 Rebuild the copy OPAC visibility cache.  Useful during migrations.
3663 $$;
3664
3665 SELECT asset.refresh_opac_visible_copies_mat_view();
3666 CREATE INDEX opac_visible_copies_idx1 on asset.opac_visible_copies (record, circ_lib);
3667 CREATE INDEX opac_visible_copies_copy_id_idx on asset.opac_visible_copies (copy_id);
3668 CREATE UNIQUE INDEX opac_visible_copies_once_per_record_idx on asset.opac_visible_copies (copy_id, record);
3669  
3670 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
3671 DECLARE
3672     add_query       TEXT;
3673     remove_query    TEXT;
3674     do_add          BOOLEAN := false;
3675     do_remove       BOOLEAN := false;
3676 BEGIN
3677     add_query := $$
3678             INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
3679               SELECT id, circ_lib, record FROM (
3680                 SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number
3681                   FROM  asset.copy cp
3682                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
3683                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3684                         JOIN asset.copy_location cl ON (cp.location = cl.id)
3685                         JOIN config.copy_status cs ON (cp.status = cs.id)
3686                         JOIN biblio.record_entry b ON (cn.record = b.id)
3687                   WHERE NOT cp.deleted
3688                         AND NOT cn.deleted
3689                         AND NOT b.deleted
3690                         AND cs.opac_visible
3691                         AND cl.opac_visible
3692                         AND cp.opac_visible
3693                         AND a.opac_visible
3694                             UNION
3695                 SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number
3696                   FROM  asset.copy cp
3697                         JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
3698                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3699                         JOIN asset.copy_location cl ON (cp.location = cl.id)
3700                         JOIN config.copy_status cs ON (cp.status = cs.id)
3701                   WHERE NOT cp.deleted
3702                         AND cs.opac_visible
3703                         AND cl.opac_visible
3704                         AND cp.opac_visible
3705                         AND a.opac_visible
3706                     ) AS x 
3707
3708     $$;
3709  
3710     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
3711
3712     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
3713         IF TG_OP = 'INSERT' THEN
3714             add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
3715             EXECUTE add_query;
3716             RETURN NEW;
3717         ELSE
3718             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
3719             EXECUTE remove_query;
3720             RETURN OLD;
3721         END IF;
3722     END IF;
3723
3724     IF TG_OP = 'INSERT' THEN
3725
3726         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
3727             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
3728             EXECUTE add_query;
3729         END IF;
3730
3731         RETURN NEW;
3732
3733     END IF;
3734
3735     -- handle items first, since with circulation activity
3736     -- their statuses change frequently
3737     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
3738
3739         IF OLD.location    <> NEW.location OR
3740            OLD.call_number <> NEW.call_number OR
3741            OLD.status      <> NEW.status OR
3742            OLD.circ_lib    <> NEW.circ_lib THEN
3743             -- any of these could change visibility, but
3744             -- we'll save some queries and not try to calculate
3745             -- the change directly
3746             do_remove := true;
3747             do_add := true;
3748         ELSE
3749
3750             IF OLD.deleted <> NEW.deleted THEN
3751                 IF NEW.deleted THEN
3752                     do_remove := true;
3753                 ELSE
3754                     do_add := true;
3755                 END IF;
3756             END IF;
3757
3758             IF OLD.opac_visible <> NEW.opac_visible THEN
3759                 IF OLD.opac_visible THEN
3760                     do_remove := true;
3761                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
3762                                         -- is also marked opac_visible
3763                     do_add := true;
3764                 END IF;
3765             END IF;
3766
3767         END IF;
3768
3769         IF do_remove THEN
3770             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
3771         END IF;
3772         IF do_add THEN
3773             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
3774             EXECUTE add_query;
3775         END IF;
3776
3777         RETURN NEW;
3778
3779     END IF;
3780
3781     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
3782  
3783         IF OLD.deleted AND NEW.deleted THEN -- do nothing
3784
3785             RETURN NEW;
3786  
3787         ELSIF NEW.deleted THEN -- remove rows
3788  
3789             IF TG_TABLE_NAME = 'call_number' THEN
3790                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
3791             ELSIF TG_TABLE_NAME = 'record_entry' THEN
3792                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
3793             END IF;
3794  
3795             RETURN NEW;
3796  
3797         ELSIF OLD.deleted THEN -- add rows
3798  
3799             IF TG_TABLE_NAME IN ('copy','unit') THEN
3800                 add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
3801             ELSIF TG_TABLE_NAME = 'call_number' THEN
3802                 add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
3803             ELSIF TG_TABLE_NAME = 'record_entry' THEN
3804                 add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
3805             END IF;
3806  
3807             EXECUTE add_query;
3808             RETURN NEW;
3809  
3810         END IF;
3811  
3812     END IF;
3813
3814     IF TG_TABLE_NAME = 'call_number' THEN
3815
3816         IF OLD.record <> NEW.record THEN
3817             -- call number is linked to different bib
3818             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
3819             EXECUTE remove_query;
3820             add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
3821             EXECUTE add_query;
3822         END IF;
3823
3824         RETURN NEW;
3825
3826     END IF;
3827
3828     IF TG_TABLE_NAME IN ('record_entry') THEN
3829         RETURN NEW; -- don't have 'opac_visible'
3830     END IF;
3831
3832     -- actor.org_unit, asset.copy_location, asset.copy_status
3833     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
3834
3835         RETURN NEW;
3836
3837     ELSIF NEW.opac_visible THEN -- add rows
3838
3839         IF TG_TABLE_NAME = 'org_unit' THEN
3840             add_query := add_query || 'AND cp.circ_lib = ' || NEW.id || ';';
3841         ELSIF TG_TABLE_NAME = 'copy_location' THEN
3842             add_query := add_query || 'AND cp.location = ' || NEW.id || ';';
3843         ELSIF TG_TABLE_NAME = 'copy_status' THEN
3844             add_query := add_query || 'AND cp.status = ' || NEW.id || ';';
3845         END IF;
3846  
3847         EXECUTE add_query;
3848  
3849     ELSE -- delete rows
3850
3851         IF TG_TABLE_NAME = 'org_unit' THEN
3852             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
3853         ELSIF TG_TABLE_NAME = 'copy_location' THEN
3854             remove_query := remove_query || 'location = ' || NEW.id || ');';
3855         ELSIF TG_TABLE_NAME = 'copy_status' THEN
3856             remove_query := remove_query || 'status = ' || NEW.id || ');';
3857         END IF;
3858  
3859         EXECUTE remove_query;
3860  
3861     END IF;
3862  
3863     RETURN NEW;
3864 END;
3865 $func$ LANGUAGE PLPGSQL;
3866 COMMENT ON FUNCTION asset.cache_copy_visibility() IS $$
3867 Trigger function to update the copy OPAC visiblity cache.
3868 $$;
3869
3870 CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR DELETE ON biblio.peer_bib_copy_map FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
3871
3872 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
3873 DECLARE
3874     transformed_xml TEXT;
3875     prev_xfrm       TEXT;
3876     normalizer      RECORD;
3877     xfrm            config.xml_transform%ROWTYPE;
3878     attr_value      TEXT;
3879     new_attrs       HSTORE := ''::HSTORE;
3880     attr_def        config.record_attr_definition%ROWTYPE;
3881 BEGIN
3882
3883     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
3884         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
3885         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
3886         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
3887         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
3888         RETURN NEW; -- and we're done
3889     END IF;
3890
3891     IF TG_OP = 'UPDATE' THEN -- re-ingest?
3892         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
3893
3894         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
3895             RETURN NEW;
3896         END IF;
3897     END IF;
3898
3899     -- Record authority linking
3900     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
3901     IF NOT FOUND THEN
3902         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
3903     END IF;
3904
3905     -- Flatten and insert the mfr data
3906     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
3907     IF NOT FOUND THEN
3908         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
3909
3910         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
3911         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
3912         IF NOT FOUND THEN
3913             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
3914
3915                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
3916                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
3917                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
3918                       WHERE record = NEW.id
3919                             AND tag LIKE attr_def.tag
3920                             AND CASE
3921                                 WHEN attr_def.sf_list IS NOT NULL 
3922                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
3923                                 ELSE TRUE
3924                                 END
3925                       GROUP BY tag
3926                       ORDER BY tag
3927                       LIMIT 1;
3928
3929                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
3930                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
3931
3932                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
3933
3934                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
3935             
3936                     -- See if we can skip the XSLT ... it's expensive
3937                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
3938                         -- Can't skip the transform
3939                         IF xfrm.xslt <> '---' THEN
3940                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
3941                         ELSE
3942                             transformed_xml := NEW.marc;
3943                         END IF;
3944             
3945                         prev_xfrm := xfrm.name;
3946                     END IF;
3947
3948                     IF xfrm.name IS NULL THEN
3949                         -- just grab the marcxml (empty) transform
3950                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
3951                         prev_xfrm := xfrm.name;
3952                     END IF;
3953
3954                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
3955
3956                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
3957                     SELECT  value::TEXT INTO attr_value
3958                       FROM  biblio.marc21_physical_characteristics(NEW.id)
3959                       WHERE subfield = attr_def.phys_char_sf
3960                       LIMIT 1; -- Just in case ...
3961
3962                 END IF;
3963
3964                 -- apply index normalizers to attr_value
3965                 FOR normalizer IN
3966                     SELECT  n.func AS func,
3967                             n.param_count AS param_count,
3968                             m.params AS params
3969                       FROM  config.index_normalizer n
3970                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
3971                       WHERE attr = attr_def.name
3972                       ORDER BY m.pos LOOP
3973                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
3974                             quote_literal( attr_value ) ||
3975                             CASE
3976                                 WHEN normalizer.param_count > 0
3977                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
3978                                     ELSE ''
3979                                 END ||
3980                             ')' INTO attr_value;
3981         
3982                 END LOOP;
3983
3984                 -- Add the new value to the hstore
3985                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
3986
3987             END LOOP;
3988
3989             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
3990                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
3991             ELSE
3992                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
3993             END IF;
3994
3995         END IF;
3996     END IF;
3997
3998     -- Gather and insert the field entry data
3999     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
4000
4001     -- Located URI magic
4002     IF TG_OP = 'INSERT' THEN
4003         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4004         IF NOT FOUND THEN
4005             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4006         END IF;
4007     ELSE
4008         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4009         IF NOT FOUND THEN
4010             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4011         END IF;
4012     END IF;
4013
4014     -- (re)map metarecord-bib linking
4015     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
4016         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
4017         IF NOT FOUND THEN
4018             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4019         END IF;
4020     ELSE -- we're doing an update, and we're not deleted, remap
4021         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
4022         IF NOT FOUND THEN
4023             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4024         END IF;
4025     END IF;
4026
4027     RETURN NEW;
4028 END;
4029 $func$ LANGUAGE PLPGSQL;
4030
4031 -- 0513
4032 CREATE OR REPLACE FUNCTION unapi.mra ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
4033         SELECT  XMLELEMENT(
4034                     name attributes,
4035                     XMLATTRIBUTES(
4036                         CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
4037                         'tag:open-ils.org:U2@mra/' || mra.id AS id,
4038                         'tag:open-ils.org:U2@bre/' || mra.id AS record
4039                     ),
4040                     (SELECT XMLAGG(foo.y)
4041                       FROM (SELECT XMLELEMENT(
4042                                 name field,
4043                                 XMLATTRIBUTES(
4044                                     key AS name,
4045                                     cvm.value AS "coded-value",
4046                                     rad.filter,
4047                                     rad.sorter
4048                                 ),
4049                                 x.value
4050                             )
4051                            FROM EACH(mra.attrs) AS x
4052                                 JOIN config.record_attr_definition rad ON (x.key = rad.name)
4053                                 LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = x.key AND code = x.value)
4054                         )foo(y)
4055                     )
4056                 )
4057           FROM  metabib.record_attr mra
4058           WHERE mra.id = $1;
4059 $F$ LANGUAGE SQL;
4060
4061 CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
4062 DECLARE
4063     me      biblio.record_entry%ROWTYPE;
4064     layout  unapi.bre_output_layout%ROWTYPE;
4065     xfrm    config.xml_transform%ROWTYPE;
4066     ouid    INT;
4067     tmp_xml TEXT;
4068     top_el  TEXT;
4069     output  XML;
4070     hxml    XML;
4071     axml    XML;
4072 BEGIN
4073
4074     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
4075
4076     IF ouid IS NULL THEN
4077         RETURN NULL::XML;
4078     END IF;
4079
4080     IF format = 'holdings_xml' THEN -- the special case
4081         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
4082         RETURN output;
4083     END IF;
4084
4085     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
4086
4087     IF layout.name IS NULL THEN
4088         RETURN NULL::XML;
4089     END IF;
4090
4091     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
4092
4093     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
4094
4095     -- grab SVF if we need them
4096     IF ('mra' = ANY (includes)) THEN
4097         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
4098     ELSE
4099         axml := NULL::XML;
4100     END IF;
4101
4102     -- grab hodlings if we need them
4103     IF ('holdings_xml' = ANY (includes)) THEN
4104         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
4105     ELSE
4106         hxml := NULL::XML;
4107     END IF;
4108
4109
4110     -- generate our item node
4111
4112
4113     IF format = 'marcxml' THEN
4114         tmp_xml := me.marc;
4115         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
4116            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
4117         END IF;
4118     ELSE
4119         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
4120     END IF;
4121
4122     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
4123
4124     IF axml IS NOT NULL THEN
4125         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
4126     END IF;
4127
4128     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
4129         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
4130     END IF;
4131
4132     IF ('bre.unapi' = ANY (includes)) THEN
4133         output := REGEXP_REPLACE(
4134             tmp_xml,
4135             '</' || top_el || '>(.*?)',
4136             XMLELEMENT(
4137                 name abbr,
4138                 XMLATTRIBUTES(
4139                     'http://www.w3.org/1999/xhtml' AS xmlns,
4140                     'unapi-id' AS class,
4141                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
4142                 )
4143             )::TEXT || '</' || top_el || E'>\\1'
4144         );
4145     ELSE
4146         output := tmp_xml;
4147     END IF;
4148
4149     RETURN output;
4150 END;
4151 $F$ LANGUAGE PLPGSQL;
4152
4153
4154 -- 0514
4155 CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
4156 DECLARE
4157     me      biblio.record_entry%ROWTYPE;
4158     layout  unapi.bre_output_layout%ROWTYPE;
4159     xfrm    config.xml_transform%ROWTYPE;
4160     ouid    INT;
4161     tmp_xml TEXT;
4162     top_el  TEXT;
4163     output  XML;
4164     hxml    XML;
4165     axml    XML;
4166 BEGIN
4167
4168     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
4169
4170     IF ouid IS NULL THEN
4171         RETURN NULL::XML;
4172     END IF;
4173
4174     IF format = 'holdings_xml' THEN -- the special case
4175         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
4176         RETURN output;
4177     END IF;
4178
4179     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
4180
4181     IF layout.name IS NULL THEN
4182         RETURN NULL::XML;
4183     END IF;
4184
4185     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
4186
4187     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
4188
4189     -- grab SVF if we need them
4190     IF ('mra' = ANY (includes)) THEN
4191         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
4192     ELSE
4193         axml := NULL::XML;
4194     END IF;
4195
4196     -- grab hodlings if we need them
4197     IF ('holdings_xml' = ANY (includes)) THEN
4198         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
4199     ELSE
4200         hxml := NULL::XML;
4201     END IF;
4202
4203
4204     -- generate our item node
4205
4206
4207     IF format = 'marcxml' THEN
4208         tmp_xml := me.marc;
4209         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
4210            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
4211         END IF;
4212     ELSE
4213         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
4214     END IF;
4215
4216     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
4217
4218     IF axml IS NOT NULL THEN
4219         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
4220     END IF;
4221
4222     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
4223         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
4224     END IF;
4225
4226     IF ('bre.unapi' = ANY (includes)) THEN
4227         output := REGEXP_REPLACE(
4228             tmp_xml,
4229             '</' || top_el || '>(.*?)',
4230             XMLELEMENT(
4231                 name abbr,
4232                 XMLATTRIBUTES(
4233                     'http://www.w3.org/1999/xhtml' AS xmlns,
4234                     'unapi-id' AS class,
4235                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
4236                 )
4237             )::TEXT || '</' || top_el || E'>\\1'
4238         );
4239     ELSE
4240         output := tmp_xml;
4241     END IF;
4242
4243     output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
4244     RETURN output;
4245 END;
4246 $F$ LANGUAGE PLPGSQL;
4247
4248
4249
4250 -- 0516
4251 CREATE OR REPLACE FUNCTION public.extract_acq_marc_field ( BIGINT, TEXT, TEXT) RETURNS TEXT AS $$    
4252     SELECT extract_marc_field('acq.lineitem', $1, $2, $3);
4253 $$ LANGUAGE SQL;
4254
4255 -- 0518
4256 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
4257 DECLARE
4258     rtype       TEXT;
4259     ff_pos      RECORD;
4260     tag_data    RECORD;
4261     val         TEXT;
4262 BEGIN
4263     rtype := (vandelay.marc21_record_type( marc )).code;
4264     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
4265         IF ff_pos.tag = 'ldr' THEN
4266             val := oils_xpath_string('//*[local-name()="leader"]', marc);
4267             IF val IS NOT NULL THEN
4268                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
4269                 RETURN val;
4270             END IF;
4271         ELSE 
4272             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
4273                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
4274                 RETURN val;
4275             END LOOP;
4276         END IF;
4277         val := REPEAT( ff_pos.default_val, ff_pos.length );
4278         RETURN val;
4279     END LOOP;
4280
4281     RETURN NULL;
4282 END;
4283 $func$ LANGUAGE PLPGSQL;
4284
4285
4286 -- 0519
4287 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
4288 DECLARE
4289     tag_data    TEXT;
4290     rtype       TEXT;
4291     ff_pos      RECORD;
4292     output      biblio.record_ff_map%ROWTYPE;
4293 BEGIN
4294     rtype := (vandelay.marc21_record_type( marc )).code;
4295
4296     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
4297         output.ff_name  := ff_pos.fixed_field;
4298         output.ff_value := NULL;
4299
4300         IF ff_pos.tag = 'ldr' THEN
4301             output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
4302             IF output.ff_value IS NOT NULL THEN
4303                 output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
4304                 RETURN NEXT output;
4305                 output.ff_value := NULL;
4306             END IF;
4307         ELSE
4308             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
4309                 output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
4310                 IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
4311                 RETURN NEXT output;
4312                 output.ff_value := NULL;
4313             END LOOP;
4314         END IF;
4315     
4316     END LOOP;
4317
4318     RETURN;
4319 END;
4320 $func$ LANGUAGE PLPGSQL;
4321
4322
4323 -- 0521
4324 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
4325 DECLARE
4326     uris            TEXT[];
4327     uri_xml         TEXT;
4328     uri_label       TEXT;
4329     uri_href        TEXT;
4330     uri_use         TEXT;
4331     uri_owner_list  TEXT[];
4332     uri_owner       TEXT;
4333     uri_owner_id    INT;
4334     uri_id          INT;
4335     uri_cn_id       INT;
4336     uri_map_id      INT;
4337 BEGIN
4338
4339     -- Clear any URI mappings and call numbers for this bib.
4340     -- This leads to acn / auricnm inflation, but also enables
4341     -- old acn/auricnm's to go away and for bibs to be deleted.
4342     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
4343         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
4344         DELETE FROM asset.call_number WHERE id = uri_cn_id;
4345     END LOOP;
4346
4347     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
4348     IF ARRAY_UPPER(uris,1) > 0 THEN
4349         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
4350             -- First we pull info out of the 856
4351             uri_xml     := uris[i];
4352
4353             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
4354             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()|//*[@code="u"]/text()',uri_xml))[1];
4355             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
4356             CONTINUE WHEN uri_href IS NULL OR uri_label IS NULL;
4357
4358             -- Get the distinct list of libraries wanting to use 
4359             SELECT  ARRAY_ACCUM(
4360                         DISTINCT REGEXP_REPLACE(
4361                             x,
4362                             $re$^.*?\((\w+)\).*$$re$,
4363                             E'\\1'
4364                         )
4365                     ) INTO uri_owner_list
4366               FROM  UNNEST(
4367                         oils_xpath(
4368                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
4369                             uri_xml
4370                         )
4371                     )x;
4372
4373             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
4374
4375                 -- look for a matching uri
4376                 SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
4377                 IF NOT FOUND THEN -- create one
4378                     INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
4379                     IF uri_use IS NULL THEN
4380                         SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
4381                     ELSE
4382                         SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
4383                     END IF;
4384                 END IF;
4385
4386                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
4387                     uri_owner := uri_owner_list[j];
4388
4389                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
4390                     CONTINUE WHEN NOT FOUND;
4391
4392                     -- we need a call number to link through
4393                     SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = bib_id AND label = '##URI##' AND NOT deleted;
4394                     IF NOT FOUND THEN
4395                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
4396                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
4397                         SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = bib_id AND label = '##URI##' AND NOT deleted;
4398                     END IF;
4399
4400                     -- now, link them if they're not already
4401                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
4402                     IF NOT FOUND THEN
4403                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
4404                     END IF;
4405
4406                 END LOOP;
4407
4408             END IF;
4409
4410         END LOOP;
4411     END IF;
4412
4413     RETURN;
4414 END;
4415 $func$ LANGUAGE PLPGSQL;
4416
4417
4418 -- 0522
4419 UPDATE config.org_unit_setting_type SET datatype = 'string' WHERE name = 'ui.general.button_bar';
4420
4421 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype) VALUES ('ui.general.hotkeyset', 'GUI: Default Hotkeyset', 'Default Hotkeyset for clients (filename without the .keyset).  Examples: Default, Minimal, and None', 'string');
4422
4423 UPDATE actor.org_unit_setting SET value='"circ"' WHERE name = 'ui.general.button_bar' AND value='true';
4424
4425 UPDATE actor.org_unit_setting SET value='"none"' WHERE name = 'ui.general.button_bar' AND value='false';
4426
4427
4428 -- 0523
4429 INSERT into config.org_unit_setting_type
4430 ( name, label, description, datatype, fm_class ) VALUES
4431 ( 'cat.default_copy_status_fast',
4432   oils_i18n_gettext( 'cat.default_copy_status_fast', 'Cataloging: Default copy status (fast add)', 'coust', 'label'),
4433   oils_i18n_gettext( 'cat.default_copy_status_fast', 'Default status when a copy is created using the "Fast Add" interface.', 'coust', 'description'),
4434   'link', 'ccs'
4435 );
4436
4437 INSERT into config.org_unit_setting_type
4438 ( name, label, description, datatype, fm_class ) VALUES
4439 ( 'cat.default_copy_status_normal',
4440   oils_i18n_gettext( 'cat.default_copy_status_normal', 'Cataloging: Default copy status (normal)', 'coust', 'label'),
4441   oils_i18n_gettext( 'cat.default_copy_status_normal', 'Default status when a copy is created using the normal volume/copy creator interface.', 'coust', 'description'),
4442   'link', 'ccs'
4443 );
4444
4445 -- 0524
4446 INSERT into config.org_unit_setting_type
4447 ( name, label, description, datatype ) VALUES
4448 ( 'ui.unified_volume_copy_editor',
4449   oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'GUI: Unified Volume/Item Creator/Editor', 'coust', 'label'),
4450   oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'If true combines the Volume/Copy Creator and Item Attribute Editor in some instances.', 'coust', 'description'),
4451   'bool'
4452 );
4453
4454 -- 0525
4455 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
4456 DECLARE
4457     transformed_xml TEXT;
4458     prev_xfrm       TEXT;
4459     normalizer      RECORD;
4460     xfrm            config.xml_transform%ROWTYPE;
4461     attr_value      TEXT;
4462     new_attrs       HSTORE := ''::HSTORE;
4463     attr_def        config.record_attr_definition%ROWTYPE;
4464 BEGIN
4465
4466     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
4467         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
4468         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
4469         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
4470         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
4471         RETURN NEW; -- and we're done
4472     END IF;
4473
4474     IF TG_OP = 'UPDATE' THEN -- re-ingest?
4475         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
4476
4477         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
4478             RETURN NEW;
4479         END IF;
4480     END IF;
4481
4482     -- Record authority linking
4483     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
4484     IF NOT FOUND THEN
4485         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
4486     END IF;
4487
4488     -- Flatten and insert the mfr data
4489     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
4490     IF NOT FOUND THEN
4491         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
4492
4493         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
4494         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
4495         IF NOT FOUND THEN
4496             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
4497
4498                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
4499                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
4500                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
4501                       WHERE record = NEW.id
4502                             AND tag LIKE attr_def.tag
4503                             AND CASE
4504                                 WHEN attr_def.sf_list IS NOT NULL 
4505                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
4506                                 ELSE TRUE
4507                                 END
4508                       GROUP BY tag
4509                       ORDER BY tag
4510                       LIMIT 1;
4511
4512                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
4513                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
4514
4515                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
4516
4517                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
4518             
4519                     -- See if we can skip the XSLT ... it's expensive
4520                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
4521                         -- Can't skip the transform
4522                         IF xfrm.xslt <> '---' THEN
4523                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
4524                         ELSE
4525                             transformed_xml := NEW.marc;
4526                         END IF;
4527             
4528                         prev_xfrm := xfrm.name;
4529                     END IF;
4530
4531                     IF xfrm.name IS NULL THEN
4532                         -- just grab the marcxml (empty) transform
4533                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
4534                         prev_xfrm := xfrm.name;
4535                     END IF;
4536
4537                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
4538
4539                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
4540                     SELECT  m.value INTO attr_value
4541                       FROM  biblio.marc21_physical_characteristics(NEW.id) v
4542                             JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
4543                       WHERE v.subfield = attr_def.phys_char_sf
4544                       LIMIT 1; -- Just in case ...
4545
4546                 END IF;
4547
4548                 -- apply index normalizers to attr_value
4549                 FOR normalizer IN
4550                     SELECT  n.func AS func,
4551                             n.param_count AS param_count,
4552                             m.params AS params
4553                       FROM  config.index_normalizer n
4554                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
4555                       WHERE attr = attr_def.name
4556                       ORDER BY m.pos LOOP
4557                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
4558                             quote_literal( attr_value ) ||
4559                             CASE
4560                                 WHEN normalizer.param_count > 0
4561                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
4562                                     ELSE ''
4563                                 END ||
4564                             ')' INTO attr_value;
4565         
4566                 END LOOP;
4567
4568                 -- Add the new value to the hstore
4569                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
4570
4571             END LOOP;
4572
4573             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
4574                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
4575             ELSE
4576                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
4577             END IF;
4578
4579         END IF;
4580     END IF;
4581
4582     -- Gather and insert the field entry data
4583     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
4584
4585     -- Located URI magic
4586     IF TG_OP = 'INSERT' THEN
4587         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4588         IF NOT FOUND THEN
4589             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4590         END IF;
4591     ELSE
4592         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4593         IF NOT FOUND THEN
4594             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4595         END IF;
4596     END IF;
4597
4598     -- (re)map metarecord-bib linking
4599     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
4600         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
4601         IF NOT FOUND THEN
4602             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4603         END IF;
4604     ELSE -- we're doing an update, and we're not deleted, remap
4605         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
4606         IF NOT FOUND THEN
4607             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4608         END IF;
4609     END IF;
4610
4611     RETURN NEW;
4612 END;
4613 $func$ LANGUAGE PLPGSQL;
4614
4615 ALTER TABLE config.circ_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6,2) NOT NULL DEFAULT 0.0;
4616
4617 UPDATE config.circ_matrix_weights
4618 SET marc_bib_level = marc_vr_format;
4619
4620 ALTER TABLE config.hold_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6, 2) NOT NULL DEFAULT 0.0;
4621
4622 UPDATE config.hold_matrix_weights
4623 SET marc_bib_level = marc_vr_format;
4624
4625 ALTER TABLE config.circ_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
4626
4627 ALTER TABLE config.hold_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
4628
4629 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$
4630 DECLARE
4631     cn_object       asset.call_number%ROWTYPE;
4632     rec_descriptor  metabib.rec_descriptor%ROWTYPE;
4633     cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
4634     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
4635     weights         config.circ_matrix_weights%ROWTYPE;
4636     user_age        INTERVAL;
4637     denominator     NUMERIC(6,2);
4638     row_list        INT[];
4639     result          action.found_circ_matrix_matchpoint;
4640 BEGIN
4641     -- Assume failure
4642     result.success = false;
4643
4644     -- Fetch useful data
4645     SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
4646     SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
4647
4648     -- Pre-generate this so we only calc it once
4649     IF user_object.dob IS NOT NULL THEN
4650         SELECT INTO user_age age(user_object.dob);
4651     END IF;
4652
4653     -- Grab the closest set circ weight setting.
4654     SELECT INTO weights cw.*
4655       FROM config.weight_assoc wa
4656            JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
4657            JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
4658       WHERE active
4659       ORDER BY d.distance
4660       LIMIT 1;
4661
4662     -- No weights? Bad admin! Defaults to handle that anyway.
4663     IF weights.id IS NULL THEN
4664         weights.grp                 := 11.0;
4665         weights.org_unit            := 10.0;
4666         weights.circ_modifier       := 5.0;
4667         weights.marc_type           := 4.0;
4668         weights.marc_form           := 3.0;
4669         weights.marc_bib_level      := 2.0;
4670         weights.marc_vr_format      := 2.0;
4671         weights.copy_circ_lib       := 8.0;
4672         weights.copy_owning_lib     := 8.0;
4673         weights.user_home_ou        := 8.0;
4674         weights.ref_flag            := 1.0;
4675         weights.juvenile_flag       := 6.0;
4676         weights.is_renewal          := 7.0;
4677         weights.usr_age_lower_bound := 0.0;
4678         weights.usr_age_upper_bound := 0.0;
4679     END IF;
4680
4681     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
4682     -- If you break your org tree with funky parenting this may be wrong
4683     -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
4684     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
4685     WITH all_distance(distance) AS (
4686             SELECT depth AS distance FROM actor.org_unit_type
4687         UNION
4688             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
4689         )
4690     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
4691
4692     -- Loop over all the potential matchpoints
4693     FOR cur_matchpoint IN
4694         SELECT m.*
4695           FROM  config.circ_matrix_matchpoint m
4696                 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
4697                 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
4698                 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
4699                 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
4700                 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
4701           WHERE m.active
4702                 -- Permission Groups
4703              -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
4704                 -- Org Units
4705              -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
4706                 AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
4707                 AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
4708                 AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
4709                 -- Circ Type
4710                 AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
4711                 -- Static User Checks
4712                 AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
4713                 AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
4714                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
4715                 -- Static Item Checks
4716                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
4717                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
4718                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
4719                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
4720                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
4721                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
4722           ORDER BY
4723                 -- Permission Groups
4724                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
4725                 -- Org Units
4726                 CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
4727                 CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
4728                 CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
4729                 CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
4730                 -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
4731                 CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
4732                 -- Static User Checks
4733                 CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
4734                 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
4735                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
4736                 -- Static Item Checks
4737                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
4738                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
4739                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
4740                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
4741                 CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
4742                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
4743                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
4744                 m.id LOOP
4745
4746         -- Record the full matching row list
4747         row_list := row_list || cur_matchpoint.id;
4748
4749         -- No matchpoint yet?
4750         IF matchpoint.id IS NULL THEN
4751             -- Take the entire matchpoint as a starting point
4752             matchpoint := cur_matchpoint;
4753             CONTINUE; -- No need to look at this row any more.
4754         END IF;
4755
4756         -- Incomplete matchpoint?
4757         IF matchpoint.circulate IS NULL THEN
4758             matchpoint.circulate := cur_matchpoint.circulate;
4759         END IF;
4760         IF matchpoint.duration_rule IS NULL THEN
4761             matchpoint.duration_rule := cur_matchpoint.duration_rule;
4762         END IF;
4763         IF matchpoint.recurring_fine_rule IS NULL THEN
4764             matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
4765         END IF;
4766         IF matchpoint.max_fine_rule IS NULL THEN
4767             matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
4768         END IF;
4769         IF matchpoint.hard_due_date IS NULL THEN
4770             matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
4771         END IF;
4772         IF matchpoint.total_copy_hold_ratio IS NULL THEN
4773             matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
4774         END IF;
4775         IF matchpoint.available_copy_hold_ratio IS NULL THEN
4776             matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
4777         END IF;
4778         IF matchpoint.renewals IS NULL THEN
4779             matchpoint.renewals := cur_matchpoint.renewals;
4780         END IF;
4781         IF matchpoint.grace_period IS NULL THEN
4782             matchpoint.grace_period := cur_matchpoint.grace_period;
4783         END IF;
4784     END LOOP;
4785
4786     -- Check required fields
4787     IF matchpoint.circulate             IS NOT NULL AND
4788        matchpoint.duration_rule         IS NOT NULL AND
4789        matchpoint.recurring_fine_rule   IS NOT NULL AND
4790        matchpoint.max_fine_rule         IS NOT NULL THEN
4791         -- All there? We have a completed match.
4792         result.success := true;
4793     END IF;
4794
4795     -- Include the assembled matchpoint, even if it isn't complete
4796     result.matchpoint := matchpoint;
4797
4798     -- Include (for debugging) the full list of matching rows
4799     result.buildrows := row_list;
4800
4801     -- Hand the result back to caller
4802     RETURN result;
4803 END;
4804 $func$ LANGUAGE plpgsql;
4805
4806 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
4807   RETURNS integer AS
4808 $func$
4809 DECLARE
4810     requestor_object    actor.usr%ROWTYPE;
4811     user_object         actor.usr%ROWTYPE;
4812     item_object         asset.copy%ROWTYPE;
4813     item_cn_object      asset.call_number%ROWTYPE;
4814     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
4815     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
4816     weights             config.hold_matrix_weights%ROWTYPE;
4817     denominator         NUMERIC(6,2);
4818 BEGIN
4819     SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
4820     SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
4821     SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
4822     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
4823     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
4824
4825     -- The item's owner should probably be the one determining if the item is holdable
4826     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
4827     -- This flag will allow for setting it to the owning library (where the call number "lives")
4828     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
4829
4830     -- Grab the closest set circ weight setting.
4831     IF NOT FOUND THEN
4832         -- Default to circ library
4833         SELECT INTO weights hw.*
4834           FROM config.weight_assoc wa
4835                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
4836                JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
4837           WHERE active
4838           ORDER BY d.distance
4839           LIMIT 1;
4840     ELSE
4841         -- Flag is set, use owning library
4842         SELECT INTO weights hw.*
4843           FROM config.weight_assoc wa
4844                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
4845                JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
4846           WHERE active
4847           ORDER BY d.distance
4848           LIMIT 1;
4849     END IF;
4850
4851     -- No weights? Bad admin! Defaults to handle that anyway.
4852     IF weights.id IS NULL THEN
4853         weights.user_home_ou    := 5.0;
4854         weights.request_ou      := 5.0;
4855         weights.pickup_ou       := 5.0;
4856         weights.item_owning_ou  := 5.0;
4857         weights.item_circ_ou    := 5.0;
4858         weights.usr_grp         := 7.0;
4859         weights.requestor_grp   := 8.0;
4860         weights.circ_modifier   := 4.0;
4861         weights.marc_type       := 3.0;
4862         weights.marc_form       := 2.0;
4863         weights.marc_bib_level  := 1.0;
4864         weights.marc_vr_format  := 1.0;
4865         weights.juvenile_flag   := 4.0;
4866         weights.ref_flag        := 0.0;
4867     END IF;
4868
4869     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
4870     -- If you break your org tree with funky parenting this may be wrong
4871     -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
4872     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
4873     WITH all_distance(distance) AS (
4874             SELECT depth AS distance FROM actor.org_unit_type
4875         UNION
4876             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
4877         )
4878     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
4879
4880     -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
4881     -- This may be better implemented as part of the upgrade script?
4882     -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
4883     -- Then remove this flag, of course.
4884     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
4885
4886     IF FOUND THEN
4887         -- Note: This, to me, is REALLY hacky. I put it in anyway.
4888         -- If you can't tell, this is a single call swap on two variables.
4889         SELECT INTO user_object.profile, requestor_object.profile
4890                     requestor_object.profile, user_object.profile;
4891     END IF;
4892
4893     -- Select the winning matchpoint into the matchpoint variable for returning
4894     SELECT INTO matchpoint m.*
4895       FROM  config.hold_matrix_matchpoint m
4896             /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
4897             LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
4898             LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
4899             LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
4900             LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
4901             LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
4902             LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
4903       WHERE m.active
4904             -- Permission Groups
4905          -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
4906             AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
4907             -- Org Units
4908             AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
4909             AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
4910             AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
4911             AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
4912             AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
4913             -- Static User Checks
4914             AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
4915             -- Static Item Checks
4916             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
4917             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
4918             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
4919             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
4920             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
4921             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
4922       ORDER BY
4923             -- Permission Groups
4924             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
4925             CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
4926             -- Org Units
4927             CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
4928             CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
4929             CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
4930             CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
4931             CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
4932             -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
4933             CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
4934             -- Static Item Checks
4935             CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
4936             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
4937             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
4938             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
4939             CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
4940             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
4941             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
4942             m.id;
4943
4944     -- Return just the ID for now
4945     RETURN matchpoint.id;
4946 END;
4947 $func$ LANGUAGE 'plpgsql';
4948
4949 -- 0528
4950 CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
4951 use strict;
4952 use MARC::Record;
4953 use MARC::File::XML (BinaryEncoding => 'UTF-8');
4954 use MARC::Charset;
4955 use Encode;
4956 use Unicode::Normalize;
4957
4958 MARC::Charset->assume_unicode(1);
4959
4960 my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
4961 my $schema = $_TD->{table_schema};
4962 my $rec_id = $_TD->{new}{id};
4963
4964 # Short-circuit if maintaining control numbers per MARC21 spec is not enabled
4965 my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
4966 if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
4967     return;
4968 }
4969
4970 # Get the control number identifier from an OU setting based on $_TD->{new}{owner}
4971 my $ou_cni = 'EVRGRN';
4972
4973 my $owner;
4974 if ($schema eq 'serial') {
4975     $owner = $_TD->{new}{owning_lib};
4976 } else {
4977     # are.owner and bre.owner can be null, so fall back to the consortial setting
4978     $owner = $_TD->{new}{owner} || 1;
4979 }
4980
4981 my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
4982 if ($ous_rv->{processed}) {
4983     $ou_cni = $ous_rv->{rows}[0]->{value};
4984     $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
4985 } else {
4986     # Fall back to the shortname of the OU if there was no OU setting
4987     $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
4988     if ($ous_rv->{processed}) {
4989         $ou_cni = $ous_rv->{rows}[0]->{shortname};
4990     }
4991 }
4992
4993 my ($create, $munge) = (0, 0);
4994
4995 my @scns = $record->field('035');
4996
4997 foreach my $id_field ('001', '003') {
4998     my $spec_value;
4999     my @controls = $record->field($id_field);
5000
5001     if ($id_field eq '001') {
5002         $spec_value = $rec_id;
5003     } else {
5004         $spec_value = $ou_cni;
5005     }
5006
5007     # Create the 001/003 if none exist
5008     if (scalar(@controls) == 1) {
5009         # Only one field; check to see if we need to munge it
5010         unless (grep $_->data() eq $spec_value, @controls) {
5011             $munge = 1;
5012         }
5013     } else {
5014         # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
5015         foreach my $control (@controls) {
5016             unless ($control->data() eq $spec_value) {
5017                 $record->delete_field($control);
5018             }
5019         }
5020         $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
5021         $create = 1;
5022     }
5023 }
5024
5025 # Now, if we need to munge the 001, we will first push the existing 001/003
5026 # into the 035; but if the record did not have one (and one only) 001 and 003
5027 # to begin with, skip this process
5028 if ($munge and not $create) {
5029     my $scn = "(" . $record->field('003')->data() . ")" . $record->field('001')->data();
5030
5031     # Do not create duplicate 035 fields
5032     unless (grep $_->subfield('a') eq $scn, @scns) {
5033         $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
5034     }
5035 }
5036
5037 # Set the 001/003 and update the MARC
5038 if ($create or $munge) {
5039     $record->field('001')->data($rec_id);
5040     $record->field('003')->data($ou_cni);
5041
5042     my $xml = $record->as_xml_record();
5043     $xml =~ s/\n//sgo;
5044     $xml =~ s/^<\?xml.+\?\s*>//go;
5045     $xml =~ s/>\s+</></go;
5046     $xml =~ s/\p{Cc}//go;
5047
5048     # Embed a version of OpenILS::Application::AppUtils->entityize()
5049     # to avoid having to set PERL5LIB for PostgreSQL as well
5050
5051     # If we are going to convert non-ASCII characters to XML entities,
5052     # we had better be dealing with a UTF8 string to begin with
5053     $xml = decode_utf8($xml);
5054
5055     $xml = NFC($xml);
5056
5057     # Convert raw ampersands to entities
5058     $xml =~ s/&(?!\S+;)/&amp;/gso;
5059
5060     # Convert Unicode characters to entities
5061     $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
5062
5063     $xml =~ s/[\x00-\x1f]//go;
5064     $_TD->{new}{marc} = $xml;
5065
5066     return "MODIFY";
5067 }
5068
5069 return;
5070 $func$ LANGUAGE PLPERLU;
5071
5072 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT ) RETURNS TEXT AS $func$
5073
5074     use MARC::Record;
5075     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5076     use MARC::Charset;
5077
5078     MARC::Charset->assume_unicode(1);
5079
5080     my $xml = shift;
5081     my $r = MARC::Record->new_from_xml( $xml );
5082
5083     return undef unless ($r);
5084
5085     my $id = shift() || $r->subfield( '901' => 'c' );
5086     $id =~ s/^\s*(?:\([^)]+\))?\s*(.+)\s*?$/$1/;
5087     return undef unless ($id); # We need an ID!
5088
5089     my $tmpl = MARC::Record->new();
5090     $tmpl->encoding( 'UTF-8' );
5091
5092     my @rule_fields;
5093     for my $field ( $r->field( '1..' ) ) { # Get main entry fields from the authority record
5094
5095         my $tag = $field->tag;
5096         my $i1 = $field->indicator(1);
5097         my $i2 = $field->indicator(2);
5098         my $sf = join '', map { $_->[0] } $field->subfields;
5099         my @data = map { @$_ } $field->subfields;
5100
5101         my @replace_them;
5102
5103         # Map the authority field to bib fields it can control.
5104         if ($tag >= 100 and $tag <= 111) {       # names
5105             @replace_them = map { $tag + $_ } (0, 300, 500, 600, 700);
5106         } elsif ($tag eq '130') {                # uniform title
5107             @replace_them = qw/130 240 440 730 830/;
5108         } elsif ($tag >= 150 and $tag <= 155) {  # subjects
5109             @replace_them = ($tag + 500);
5110         } elsif ($tag >= 180 and $tag <= 185) {  # floating subdivisions
5111             @replace_them = qw/100 400 600 700 800 110 410 610 710 810 111 411 611 711 811 130 240 440 730 830 650 651 655/;
5112         } else {
5113             next;
5114         }
5115
5116         # Dummy up the bib-side data
5117         $tmpl->append_fields(
5118             map {
5119                 MARC::Field->new( $_, $i1, $i2, @data )
5120             } @replace_them
5121         );
5122
5123         # Construct some 'replace' rules
5124         push @rule_fields, map { $_ . $sf . '[0~\)' .$id . '$]' } @replace_them;
5125     }
5126
5127     # Insert the replace rules into the template
5128     $tmpl->append_fields(
5129         MARC::Field->new( '905' => ' ' => ' ' => 'r' => join(',', @rule_fields ) )
5130     );
5131
5132     $xml = $tmpl->as_xml_record;
5133     $xml =~ s/^<\?.+?\?>$//mo;
5134     $xml =~ s/\n//sgo;
5135     $xml =~ s/>\s+</></sgo;
5136
5137     return $xml;
5138
5139 $func$ LANGUAGE PLPERLU;
5140
5141 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
5142
5143     use MARC::Record;
5144     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5145     use MARC::Charset;
5146     use strict;
5147
5148     MARC::Charset->assume_unicode(1);
5149
5150     my $target_xml = shift;
5151     my $source_xml = shift;
5152     my $field_spec = shift;
5153     my $force_add = shift || 0;
5154
5155     my $target_r = MARC::Record->new_from_xml( $target_xml );
5156     my $source_r = MARC::Record->new_from_xml( $source_xml );
5157
5158     return $target_xml unless ($target_r && $source_r);
5159
5160     my @field_list = split(',', $field_spec);
5161
5162     my %fields;
5163     for my $f (@field_list) {
5164         $f =~ s/^\s*//; $f =~ s/\s*$//;
5165         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
5166             my $field = $1;
5167             $field =~ s/\s+//;
5168             my $sf = $2;
5169             $sf =~ s/\s+//;
5170             my $match = $3;
5171             $match =~ s/^\s*//; $match =~ s/\s*$//;
5172             $fields{$field} = { sf => [ split('', $sf) ] };
5173             if ($match) {
5174                 my ($msf,$mre) = split('~', $match);
5175                 if (length($msf) > 0 and length($mre) > 0) {
5176                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
5177                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
5178                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
5179                 }
5180             }
5181         }
5182     }
5183
5184     for my $f ( keys %fields) {
5185         if ( @{$fields{$f}{sf}} ) {
5186             for my $from_field ($source_r->field( $f )) {
5187                 my @tos = $target_r->field( $f );
5188                 if (!@tos) {
5189                     next if (exists($fields{$f}{match}) and !$force_add);
5190                     my @new_fields = map { $_->clone } $source_r->field( $f );
5191                     $target_r->insert_fields_ordered( @new_fields );
5192                 } else {
5193                     for my $to_field (@tos) {
5194                         if (exists($fields{$f}{match})) {
5195                             next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
5196                         }
5197                         my @new_sf = map { ($_ => $from_field->subfield($_)) } @{$fields{$f}{sf}};
5198                         $to_field->add_subfields( @new_sf );
5199                     }
5200                 }
5201             }
5202         } else {
5203             my @new_fields = map { $_->clone } $source_r->field( $f );
5204             $target_r->insert_fields_ordered( @new_fields );
5205         }
5206     }
5207
5208     $target_xml = $target_r->as_xml_record;
5209     $target_xml =~ s/^<\?.+?\?>$//mo;
5210     $target_xml =~ s/\n//sgo;
5211     $target_xml =~ s/>\s+</></sgo;
5212
5213     return $target_xml;
5214
5215 $_$ LANGUAGE PLPERLU;
5216
5217 CREATE OR REPLACE FUNCTION authority.normalize_heading( TEXT ) RETURNS TEXT AS $func$
5218     use strict;
5219     use warnings;
5220
5221     use utf8;
5222     use MARC::Record;
5223     use MARC::File::XML (BinaryEncoding => 'UTF8');
5224     use MARC::Charset;
5225     use UUID::Tiny ':std';
5226
5227     MARC::Charset->assume_unicode(1);
5228
5229     my $xml = shift() or return undef;
5230
5231     my $r;
5232
5233     # Prevent errors in XML parsing from blowing out ungracefully
5234     eval {
5235         $r = MARC::Record->new_from_xml( $xml );
5236         1;
5237     } or do {
5238        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
5239     };
5240
5241     if (!$r) {
5242        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
5243     }
5244
5245     # From http://www.loc.gov/standards/sourcelist/subject.html
5246     my $thes_code_map = {
5247         a => 'lcsh',
5248         b => 'lcshac',
5249         c => 'mesh',
5250         d => 'nal',
5251         k => 'cash',
5252         n => 'notapplicable',
5253         r => 'aat',
5254         s => 'sears',
5255         v => 'rvm',
5256     };
5257
5258     # Default to "No attempt to code" if the leader is horribly broken
5259     my $fixed_field = $r->field('008');
5260     my $thes_char = '|';
5261     if ($fixed_field) { 
5262         $thes_char = substr($fixed_field->data(), 11, 1) || '|';
5263     }
5264
5265     my $thes_code = 'UNDEFINED';
5266
5267     if ($thes_char eq 'z') {
5268         # Grab the 040 $f per http://www.loc.gov/marc/authority/ad040.html
5269         $thes_code = $r->subfield('040', 'f') || 'UNDEFINED';
5270     } elsif ($thes_code_map->{$thes_char}) {
5271         $thes_code = $thes_code_map->{$thes_char};
5272     }
5273
5274     my $auth_txt = '';
5275     my $head = $r->field('1..');
5276     if ($head) {
5277         # Concatenate all of these subfields together, prefixed by their code
5278         # to prevent collisions along the lines of "Fiction, North Carolina"
5279         foreach my $sf ($head->subfields()) {
5280             $auth_txt .= '‡' . $sf->[0] . ' ' . $sf->[1];
5281         }
5282     }
5283     
5284     if ($auth_txt) {
5285         my $stmt = spi_prepare('SELECT public.naco_normalize($1) AS norm_text', 'TEXT');
5286         my $result = spi_exec_prepared($stmt, $auth_txt);
5287         my $norm_txt = $result->{rows}[0]->{norm_text};
5288         spi_freeplan($stmt);
5289         undef($stmt);
5290         return $head->tag() . "_" . $thes_code . " " . $norm_txt;
5291     }
5292
5293     return 'NOHEADING_' . $thes_code . ' ' . create_uuid_as_string(UUID_MD5, $xml);
5294 $func$ LANGUAGE 'plperlu' IMMUTABLE;
5295
5296 CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
5297
5298     use MARC::Record;
5299     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5300     use MARC::Charset;
5301     use strict;
5302
5303     MARC::Charset->assume_unicode(1);
5304
5305     my $xml = shift;
5306     my $r = MARC::Record->new_from_xml( $xml );
5307
5308     return $xml unless ($r);
5309
5310     my $field_spec = shift;
5311     my @field_list = split(',', $field_spec);
5312
5313     my %fields;
5314     for my $f (@field_list) {
5315         $f =~ s/^\s*//; $f =~ s/\s*$//;
5316         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
5317             my $field = $1;
5318             $field =~ s/\s+//;
5319             my $sf = $2;
5320             $sf =~ s/\s+//;
5321             my $match = $3;
5322             $match =~ s/^\s*//; $match =~ s/\s*$//;
5323             $fields{$field} = { sf => [ split('', $sf) ] };
5324             if ($match) {
5325                 my ($msf,$mre) = split('~', $match);
5326                 if (length($msf) > 0 and length($mre) > 0) {
5327                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
5328                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
5329                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
5330                 }
5331             }
5332         }
5333     }
5334
5335     for my $f ( keys %fields) {
5336         for my $to_field ($r->field( $f )) {
5337             if (exists($fields{$f}{match})) {
5338                 next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
5339             }
5340
5341             if ( @{$fields{$f}{sf}} ) {
5342                 $to_field->delete_subfield(code => $fields{$f}{sf});
5343             } else {
5344                 $r->delete_field( $to_field );
5345             }
5346         }
5347     }
5348
5349     $xml = $r->as_xml_record;
5350     $xml =~ s/^<\?.+?\?>$//mo;
5351     $xml =~ s/\n//sgo;
5352     $xml =~ s/>\s+</></sgo;
5353
5354     return $xml;
5355
5356 $_$ LANGUAGE PLPERLU;
5357
5358 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
5359
5360 use MARC::Record;
5361 use MARC::File::XML (BinaryEncoding => 'UTF-8');
5362 use MARC::Charset;
5363
5364 MARC::Charset->assume_unicode(1);
5365
5366 my $xml = shift;
5367 my $r = MARC::Record->new_from_xml( $xml );
5368
5369 return_next( { tag => 'LDR', value => $r->leader } );
5370
5371 for my $f ( $r->fields ) {
5372         if ($f->is_control_field) {
5373                 return_next({ tag => $f->tag, value => $f->data });
5374         } else {
5375                 for my $s ($f->subfields) {
5376                         return_next({
5377                                 tag      => $f->tag,
5378                                 ind1     => $f->indicator(1),
5379                                 ind2     => $f->indicator(2),
5380                                 subfield => $s->[0],
5381                                 value    => $s->[1]
5382                         });
5383
5384                         if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
5385                                 my $trim = $f->indicator(2) || 0;
5386                                 return_next({
5387                                         tag      => 'tnf',
5388                                         ind1     => $f->indicator(1),
5389                                         ind2     => $f->indicator(2),
5390                                         subfield => 'a',
5391                                         value    => substr( $s->[1], $trim )
5392                                 });
5393                         }
5394                 }
5395         }
5396 }
5397
5398 return undef;
5399
5400 $func$ LANGUAGE PLPERLU;
5401
5402 CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authority.full_rec AS $func$
5403
5404 use MARC::Record;
5405 use MARC::File::XML (BinaryEncoding => 'UTF-8');
5406 use MARC::Charset;
5407
5408 MARC::Charset->assume_unicode(1);
5409
5410 my $xml = shift;
5411 my $r = MARC::Record->new_from_xml( $xml );
5412
5413 return_next( { tag => 'LDR', value => $r->leader } );
5414
5415 for my $f ( $r->fields ) {
5416     if ($f->is_control_field) {
5417         return_next({ tag => $f->tag, value => $f->data });
5418     } else {
5419         for my $s ($f->subfields) {
5420             return_next({
5421                 tag      => $f->tag,
5422                 ind1     => $f->indicator(1),
5423                 ind2     => $f->indicator(2),
5424                 subfield => $s->[0],
5425                 value    => $s->[1]
5426             });
5427
5428         }
5429     }
5430 }
5431
5432 return undef;
5433
5434 $func$ LANGUAGE PLPERLU;
5435
5436 -- 0530
5437 CREATE INDEX actor_usr_day_phone_idx_numeric ON actor.usr USING BTREE 
5438     (evergreen.lowercase(REGEXP_REPLACE(day_phone, '[^0-9]', '', 'g')));
5439
5440 CREATE INDEX actor_usr_evening_phone_idx_numeric ON actor.usr USING BTREE 
5441     (evergreen.lowercase(REGEXP_REPLACE(evening_phone, '[^0-9]', '', 'g')));
5442
5443 CREATE INDEX actor_usr_other_phone_idx_numeric ON actor.usr USING BTREE 
5444     (evergreen.lowercase(REGEXP_REPLACE(other_phone, '[^0-9]', '', 'g')));
5445
5446 -- 0533
5447 CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
5448 DECLARE
5449 found char := 'N';
5450 BEGIN
5451
5452     -- If there are any renewals for this circulation, don't archive or delete
5453     -- it yet.   We'll do so later, when we archive and delete the renewals.
5454
5455     SELECT 'Y' INTO found
5456     FROM action.circulation
5457     WHERE parent_circ = OLD.id
5458     LIMIT 1;
5459
5460     IF found = 'Y' THEN
5461         RETURN NULL;  -- don't delete
5462         END IF;
5463
5464     -- Archive a copy of the old row to action.aged_circulation
5465
5466     INSERT INTO action.aged_circulation
5467         (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
5468         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
5469         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
5470         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
5471         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
5472         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
5473       SELECT
5474         id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
5475         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
5476         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
5477         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
5478         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
5479         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
5480         FROM action.all_circulation WHERE id = OLD.id;
5481
5482     RETURN OLD;
5483 END;
5484 $$ LANGUAGE 'plpgsql';
5485
5486 -- 0534
5487 CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT, retargetting BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
5488 DECLARE
5489     matchpoint_id        INT;
5490     user_object        actor.usr%ROWTYPE;
5491     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
5492     standing_penalty    config.standing_penalty%ROWTYPE;
5493     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
5494     transit_source        actor.org_unit%ROWTYPE;
5495     item_object        asset.copy%ROWTYPE;
5496     item_cn_object     asset.call_number%ROWTYPE;
5497     ou_skip              actor.org_unit_setting%ROWTYPE;
5498     result            action.matrix_test_result;
5499     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
5500     hold_count        INT;
5501     hold_transit_prox    INT;
5502     frozen_hold_count    INT;
5503     context_org_list    INT[];
5504     done            BOOL := FALSE;
5505 BEGIN
5506     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
5507     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( pickup_ou );
5508
5509     result.success := TRUE;
5510
5511     -- Fail if we couldn't find a user
5512     IF user_object.id IS NULL THEN
5513         result.fail_part := 'no_user';
5514         result.success := FALSE;
5515         done := TRUE;
5516         RETURN NEXT result;
5517         RETURN;
5518     END IF;
5519
5520     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
5521
5522     -- Fail if we couldn't find a copy
5523     IF item_object.id IS NULL THEN
5524         result.fail_part := 'no_item';
5525         result.success := FALSE;
5526         done := TRUE;
5527         RETURN NEXT result;
5528         RETURN;
5529     END IF;
5530
5531     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
5532     result.matchpoint := matchpoint_id;
5533
5534     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
5535
5536     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
5537     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
5538         result.fail_part := 'circ.holds.target_skip_me';
5539         result.success := FALSE;
5540         done := TRUE;
5541         RETURN NEXT result;
5542         RETURN;
5543     END IF;
5544
5545     -- Fail if user is barred
5546     IF user_object.barred IS TRUE THEN
5547         result.fail_part := 'actor.usr.barred';
5548         result.success := FALSE;
5549         done := TRUE;
5550         RETURN NEXT result;
5551         RETURN;
5552     END IF;
5553
5554     -- Fail if we couldn't find any matchpoint (requires a default)
5555     IF matchpoint_id IS NULL THEN
5556         result.fail_part := 'no_matchpoint';
5557         result.success := FALSE;
5558         done := TRUE;
5559         RETURN NEXT result;
5560         RETURN;
5561     END IF;
5562
5563     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
5564
5565     IF hold_test.holdable IS FALSE THEN
5566         result.fail_part := 'config.hold_matrix_test.holdable';
5567         result.success := FALSE;
5568         done := TRUE;
5569         RETURN NEXT result;
5570     END IF;
5571
5572     IF hold_test.transit_range IS NOT NULL THEN
5573         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
5574         IF hold_test.distance_is_from_owner THEN
5575             SELECT INTO transit_source ou.* FROM actor.org_unit ou JOIN asset.call_number cn ON (cn.owning_lib = ou.id) WHERE cn.id = item_object.call_number;
5576         ELSE
5577             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
5578         END IF;
5579
5580         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
5581
5582         IF NOT FOUND THEN
5583             result.fail_part := 'transit_range';
5584             result.success := FALSE;
5585             done := TRUE;
5586             RETURN NEXT result;
5587         END IF;
5588     END IF;
5589  
5590     FOR standing_penalty IN
5591         SELECT  DISTINCT csp.*
5592           FROM  actor.usr_standing_penalty usp
5593                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
5594           WHERE usr = match_user
5595                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
5596                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
5597                 AND csp.block_list LIKE '%HOLD%' LOOP
5598
5599         result.fail_part := standing_penalty.name;
5600         result.success := FALSE;
5601         done := TRUE;
5602         RETURN NEXT result;
5603     END LOOP;
5604
5605     IF hold_test.stop_blocked_user IS TRUE THEN
5606         FOR standing_penalty IN
5607             SELECT  DISTINCT csp.*
5608               FROM  actor.usr_standing_penalty usp
5609                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
5610               WHERE usr = match_user
5611                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
5612                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
5613                     AND csp.block_list LIKE '%CIRC%' LOOP
5614     
5615             result.fail_part := standing_penalty.name;
5616             result.success := FALSE;
5617             done := TRUE;
5618             RETURN NEXT result;
5619         END LOOP;
5620     END IF;
5621
5622     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
5623         SELECT    INTO hold_count COUNT(*)
5624           FROM    action.hold_request
5625           WHERE    usr = match_user
5626             AND fulfillment_time IS NULL
5627             AND cancel_time IS NULL
5628             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
5629
5630         IF hold_count >= hold_test.max_holds THEN
5631             result.fail_part := 'config.hold_matrix_test.max_holds';
5632             result.success := FALSE;
5633             done := TRUE;
5634             RETURN NEXT result;
5635         END IF;
5636     END IF;
5637
5638     IF item_object.age_protect IS NOT NULL THEN
5639         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
5640
5641         IF item_object.create_date + age_protect_object.age > NOW() THEN
5642             IF hold_test.distance_is_from_owner THEN
5643                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
5644                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
5645             ELSE
5646                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
5647             END IF;
5648
5649             IF hold_transit_prox > age_protect_object.prox THEN
5650                 result.fail_part := 'config.rule_age_hold_protect.prox';
5651                 result.success := FALSE;
5652                 done := TRUE;
5653                 RETURN NEXT result;
5654             END IF;
5655         END IF;
5656     END IF;
5657
5658     IF NOT done THEN
5659         RETURN NEXT result;
5660     END IF;
5661
5662     RETURN;
5663 END;
5664 $func$ LANGUAGE plpgsql;
5665
5666 -- do potentially large updates last to save time if upgrader needs
5667 -- to manually tweak the upgrade script to resolve errors
5668
5669 -- 0505
5670 UPDATE metabib.facet_entry SET value = evergreen.force_unicode_normal_form(value,'NFC');
5671
5672 -- Update reporter.materialized_simple_record with normalized ISBN values
5673 -- This might not get all of them, but most ISBNs will have more than one hyphen
5674 DELETE FROM reporter.materialized_simple_record WHERE id IN (
5675     SELECT record FROM metabib.full_rec WHERE tag = '020' AND subfield IN ('a', 'z') AND value LIKE '%-%-%'
5676 );
5677
5678 INSERT INTO reporter.materialized_simple_record
5679     SELECT DISTINCT rossr.* FROM reporter.old_super_simple_record rossr INNER JOIN metabib.full_rec mfr ON mfr.record = rossr.id
5680         WHERE mfr.tag = '020' AND mfr.subfield IN ('a', 'z') AND mfr.value LIKE '%-%-%'
5681 ;
5682
5683 INSERT INTO config.upgrade_log (version) VALUES ('0542'); -- phasefx
5684
5685 INSERT INTO permission.perm_list
5686     SELECT  np.*
5687       FROM  (VALUES
5688                 (485, 'CREATE_VOLUME_SUFFIX', oils_i18n_gettext(485, 'Create suffix label definition.', 'ppl', 'description'))
5689                 ,(486, 'UPDATE_VOLUME_SUFFIX', oils_i18n_gettext(486, 'Update suffix label definition.', 'ppl', 'description'))
5690                 ,(487, 'DELETE_VOLUME_SUFFIX', oils_i18n_gettext(487, 'Delete suffix label definition.', 'ppl', 'description'))
5691                 ,(488, 'CREATE_VOLUME_PREFIX', oils_i18n_gettext(488, 'Create prefix label definition.', 'ppl', 'description'))
5692                 ,(489, 'UPDATE_VOLUME_PREFIX', oils_i18n_gettext(489, 'Update prefix label definition.', 'ppl', 'description'))
5693                 ,(490, 'DELETE_VOLUME_PREFIX', oils_i18n_gettext(490, 'Delete prefix label definition.', 'ppl', 'description'))
5694                 ,(491, 'CREATE_MONOGRAPH_PART', oils_i18n_gettext(491, 'Create monograph part definition.', 'ppl', 'description'))
5695                 ,(492, 'UPDATE_MONOGRAPH_PART', oils_i18n_gettext(492, 'Update monograph part definition.', 'ppl', 'description'))
5696                 ,(493, 'DELETE_MONOGRAPH_PART', oils_i18n_gettext(493, 'Delete monograph part definition.', 'ppl', 'description'))
5697                 ,(494, 'ADMIN_CODED_VALUE', oils_i18n_gettext(494, 'Create/Update/Delete SVF Record Attribute Coded Value Map', 'ppl', 'description'))
5698                 ,(495, 'ADMIN_SERIAL_ITEM', oils_i18n_gettext(495, 'Create/Retrieve/Update/Delete Serial Item', 'ppl', 'description'))
5699                 ,(496, 'ADMIN_SVF', oils_i18n_gettext(496, 'Create/Update/Delete SVF Record Attribute Defintion', 'ppl', 'description'))
5700                 ,(497, 'CREATE_BIB_PTYPE', oils_i18n_gettext(497, 'Create Bibliographic Record Peer Type', 'ppl', 'description'))
5701                 ,(498, 'CREATE_PURCHASE_REQUEST', oils_i18n_gettext(498, 'Create User Purchase Request', 'ppl', 'description'))
5702                 ,(499, 'DELETE_BIB_PTYPE', oils_i18n_gettext(499, 'Delete Bibliographic Record Peer Type', 'ppl', 'description'))
5703                 ,(500, 'MAP_MONOGRAPH_PART', oils_i18n_gettext(500, 'Create/Update/Delete Copy Monograph Part Map', 'ppl', 'description'))
5704                 ,(501, 'MARK_ITEM_MISSING_PIECES', oils_i18n_gettext(501, 'Allows the Mark Item Missing Pieces action.', 'ppl', 'description'))
5705                 ,(502, 'UPDATE_BIB_PTYPE', oils_i18n_gettext(502, 'Update Bibliographic Record Peer Type', 'ppl', 'description'))
5706                 ,(503, 'UPDATE_HOLD_REQUEST_TIME', oils_i18n_gettext(503, 'Allows editing of a hold''s request time, and/or its Cut-in-line/Top-of-queue flag.', 'ppl', 'description'))
5707                 ,(504, 'UPDATE_PICKLIST', oils_i18n_gettext(504, 'Allows update/re-use of an acquisitions pick/selection list.', 'ppl', 'description'))
5708                 ,(505, 'UPDATE_WORKSTATION', oils_i18n_gettext(505, 'Allows update of a workstation during workstation registration override.', 'ppl', 'description'))
5709                 ,(506, 'VIEW_USER_SETTING_TYPE', oils_i18n_gettext(506, 'Allows viewing of configurable user setting types.', 'ppl', 'description'))
5710             ) AS np(id,code,description)
5711             LEFT JOIN permission.perm_list pl USING (code)
5712       WHERE pl.id IS NULL;
5713 ;
5714
5715
5716 -- add new perms AND catch up on some missed upgrade data, if needed
5717
5718 -- Prevent conflicts with existing custom permission groups; as of 2.0.10, 
5719 -- highest permission.grp_tree ID was 10 for Local System Administrator
5720 UPDATE permission.grp_tree SET id = id + 100 WHERE id > 10;
5721 UPDATE permission.grp_tree SET parent = parent + 100 WHERE parent > 10;
5722 UPDATE actor.usr SET profile = profile + 100 WHERE profile > 10;
5723 UPDATE permission.grp_perm_map SET grp = grp + 100 WHERE grp > 10;
5724 UPDATE permission.usr_grp_map SET grp = grp + 100 WHERE grp > 10;
5725 UPDATE permission.grp_penalty_threshold SET grp = grp + 100 WHERE grp > 10;
5726 UPDATE config.circ_matrix_matchpoint SET grp = grp + 100 WHERE grp > 10;
5727 UPDATE config.hold_matrix_matchpoint SET requestor_grp = requestor_grp + 100 WHERE requestor_grp > 10;
5728 UPDATE config.hold_matrix_matchpoint SET usr_grp = usr_grp + 100 WHERE usr_grp > 10;
5729
5730 -- we could get away from these fixed-id inserts here, but then this
5731 -- upgrade would be ahead of the mainline, I think
5732
5733 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5734         SELECT 8, oils_i18n_gettext(8, 'Cataloging Administrator', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.cat_admin'
5735         WHERE NOT EXISTS (
5736                 SELECT 1
5737                 FROM permission.grp_tree
5738                 WHERE
5739                         id = 8
5740         );
5741
5742 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5743         SELECT 9, oils_i18n_gettext(9, 'Circulation Administrator', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.circ_admin'
5744         WHERE NOT EXISTS (
5745                 SELECT 1
5746                 FROM permission.grp_tree
5747                 WHERE
5748                         id = 9
5749         );
5750
5751 UPDATE permission.grp_tree SET description = oils_i18n_gettext(10, 'Can do anything at the Branch level', 'pgt', 'description') WHERE id = 10;
5752
5753 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5754         SELECT 11, oils_i18n_gettext(11, 'Serials', 'pgt', 'name'), 3, oils_i18n_gettext(11, 'Serials (includes admin features)', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.serials'
5755         WHERE NOT EXISTS (
5756                 SELECT 1
5757                 FROM permission.grp_tree
5758                 WHERE
5759                         id = 11
5760         );
5761
5762 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5763         SELECT 12, oils_i18n_gettext(12, 'System Administrator', 'pgt', 'name'), 3, oils_i18n_gettext(12, 'Can do anything at the System level', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.admin.system_admin'
5764         WHERE NOT EXISTS (
5765                 SELECT 1
5766                 FROM permission.grp_tree
5767                 WHERE
5768                         id = 12
5769         );
5770
5771 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5772         SELECT 13, oils_i18n_gettext(13, 'Global Administrator', 'pgt', 'name'), 3, oils_i18n_gettext(13, 'Can do anything at the Consortium level', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.admin.global_admin'
5773         WHERE NOT EXISTS (
5774                 SELECT 1
5775                 FROM permission.grp_tree
5776                 WHERE
5777                         id = 13
5778         );
5779
5780 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5781         SELECT 14, oils_i18n_gettext(14, 'Data Review', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.data_review'
5782         WHERE NOT EXISTS (
5783                 SELECT 1
5784                 FROM permission.grp_tree
5785                 WHERE
5786                         id = 14
5787         );
5788
5789 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5790         SELECT 15, oils_i18n_gettext(15, 'Volunteers', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.volunteers'
5791         WHERE NOT EXISTS (
5792                 SELECT 1
5793                 FROM permission.grp_tree
5794                 WHERE
5795                         id = 15
5796         );
5797
5798 SELECT SETVAL('permission.grp_tree_id_seq'::TEXT, (SELECT MAX(id) FROM permission.grp_tree));
5799
5800 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5801         SELECT
5802                 pgt.id, perm.id, aout.depth, TRUE
5803         FROM
5804                 permission.grp_tree pgt,
5805                 permission.perm_list perm,
5806                 actor.org_unit_type aout
5807         WHERE
5808                 pgt.name = 'Cataloging Administrator' AND
5809                 aout.name = 'Consortium' AND
5810                 perm.code IN (
5811                         'ADMIN_IMPORT_ITEM_ATTR_DEF',
5812                         'ADMIN_MERGE_PROFILE',
5813                         'CREATE_AUTHORITY_IMPORT_IMPORT_DEF',
5814                         'CREATE_BIB_IMPORT_FIELD_DEF',
5815                         'CREATE_BIB_PTYPE',
5816                         'CREATE_BIB_SOURCE',
5817                         'CREATE_IMPORT_ITEM_ATTR_DEF',
5818                         'CREATE_IMPORT_TRASH_FIELD',
5819                         'CREATE_MERGE_PROFILE',
5820                         'CREATE_MONOGRAPH_PART',
5821                         'CREATE_VOLUME_PREFIX',
5822                         'CREATE_VOLUME_SUFFIX',
5823                         'DELETE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
5824                         'DELETE_BIB_PTYPE',
5825                         'DELETE_BIB_SOURCE',
5826                         'DELETE_IMPORT_ITEM_ATTR_DEF',
5827                         'DELETE_IMPORT_TRASH_FIELD',
5828                         'DELETE_MERGE_PROFILE',
5829                         'DELETE_MONOGRAPH_PART',
5830                         'DELETE_VOLUME_PREFIX',
5831                         'DELETE_VOLUME_SUFFIX',
5832                         'MAP_MONOGRAPH_PART',
5833                         'UPDATE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
5834                         'UPDATE_BIB_IMPORT_IMPORT_FIELD_DEF',
5835                         'UPDATE_BIB_PTYPE',
5836                         'UPDATE_IMPORT_ITEM_ATTR_DEF',
5837                         'UPDATE_IMPORT_TRASH_FIELD',
5838                         'UPDATE_MERGE_PROFILE',
5839                         'UPDATE_MONOGRAPH_PART',
5840                         'UPDATE_VOLUME_PREFIX',
5841                         'UPDATE_VOLUME_SUFFIX'
5842                 ) AND NOT EXISTS (
5843                         SELECT 1
5844                         FROM permission.grp_perm_map AS map
5845                         WHERE
5846                                 map.grp = pgt.id
5847                                 AND map.perm = perm.id
5848                 );
5849
5850 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5851         SELECT
5852                 pgt.id, perm.id, aout.depth, TRUE
5853         FROM
5854                 permission.grp_tree pgt,
5855                 permission.perm_list perm,
5856                 actor.org_unit_type aout
5857         WHERE
5858                 pgt.name = 'Circulation Administrator' AND
5859                 aout.name = 'Branch' AND
5860                 perm.code IN (
5861                         'DELETE_USER'
5862                 ) AND NOT EXISTS (
5863                         SELECT 1
5864                         FROM permission.grp_perm_map AS map
5865                         WHERE
5866                                 map.grp = pgt.id
5867                                 AND map.perm = perm.id
5868                 );
5869
5870 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5871         SELECT
5872                 pgt.id, perm.id, aout.depth, TRUE
5873         FROM
5874                 permission.grp_tree pgt,
5875                 permission.perm_list perm,
5876                 actor.org_unit_type aout
5877         WHERE
5878                 pgt.name = 'Circulation Administrator' AND
5879                 aout.name = 'Consortium' AND
5880                 perm.code IN (
5881                         'ADMIN_MAX_FINE_RULE',
5882                         'CREATE_CIRC_DURATION',
5883                         'DELETE_CIRC_DURATION',
5884                         'MARK_ITEM_MISSING_PIECES',
5885                         'UPDATE_CIRC_DURATION',
5886                         'UPDATE_HOLD_REQUEST_TIME',
5887                         'UPDATE_NET_ACCESS_LEVEL',
5888                         'VIEW_CIRC_MATRIX_MATCHPOINT',
5889                         'VIEW_HOLD_MATRIX_MATCHPOINT'
5890                 ) AND NOT EXISTS (
5891                         SELECT 1
5892                         FROM permission.grp_perm_map AS map
5893                         WHERE
5894                                 map.grp = pgt.id
5895                                 AND map.perm = perm.id
5896                 );
5897
5898 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5899         SELECT
5900                 pgt.id, perm.id, aout.depth, TRUE
5901         FROM
5902                 permission.grp_tree pgt,
5903                 permission.perm_list perm,
5904                 actor.org_unit_type aout
5905         WHERE
5906                 pgt.name = 'Circulation Administrator' AND
5907                 aout.name = 'System' AND
5908                 perm.code IN (
5909                         'ADMIN_BOOKING_RESERVATION',
5910                         'ADMIN_BOOKING_RESERVATION_ATTR_MAP',
5911                         'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP',
5912                         'ADMIN_BOOKING_RESOURCE',
5913                         'ADMIN_BOOKING_RESOURCE_ATTR',
5914                         'ADMIN_BOOKING_RESOURCE_ATTR_MAP',
5915                         'ADMIN_BOOKING_RESOURCE_ATTR_VALUE',
5916                         'ADMIN_BOOKING_RESOURCE_TYPE',
5917                         'ADMIN_COPY_LOCATION_ORDER',
5918                         'ADMIN_HOLD_CANCEL_CAUSE',
5919                         'ASSIGN_GROUP_PERM',
5920                         'BAR_PATRON',
5921                         'COPY_HOLDS',
5922                         'COPY_TRANSIT_RECEIVE',
5923                         'CREATE_BILL',
5924                         'CREATE_BILLING_TYPE',
5925                         'CREATE_NON_CAT_TYPE',
5926                         'CREATE_PATRON_STAT_CAT',
5927                         'CREATE_PATRON_STAT_CAT_ENTRY',
5928                         'CREATE_PATRON_STAT_CAT_ENTRY_MAP',
5929                         'CREATE_USER_GROUP_LINK',
5930                         'DELETE_BILLING_TYPE',
5931                         'DELETE_NON_CAT_TYPE',
5932                         'DELETE_PATRON_STAT_CAT',
5933                         'DELETE_PATRON_STAT_CAT_ENTRY',
5934                         'DELETE_PATRON_STAT_CAT_ENTRY_MAP',
5935                         'DELETE_TRANSIT',
5936                         'group_application.user.staff',
5937                         'MANAGE_BAD_DEBT',
5938                         'MARK_ITEM_AVAILABLE',
5939                         'MARK_ITEM_BINDERY',
5940                         'MARK_ITEM_CHECKED_OUT',
5941                         'MARK_ITEM_ILL',
5942                         'MARK_ITEM_IN_PROCESS',
5943                         'MARK_ITEM_IN_TRANSIT',
5944                         'MARK_ITEM_LOST',
5945                         'MARK_ITEM_MISSING',
5946                         'MARK_ITEM_ON_HOLDS_SHELF',
5947                         'MARK_ITEM_ON_ORDER',
5948                         'MARK_ITEM_RESHELVING',
5949                         'MERGE_USERS',
5950                         'money.collections_tracker.create',
5951                         'money.collections_tracker.delete',
5952                         'OFFLINE_EXECUTE',
5953                         'OFFLINE_UPLOAD',
5954                         'OFFLINE_VIEW',
5955                         'REMOVE_USER_GROUP_LINK',
5956                         'SET_CIRC_CLAIMS_RETURNED',
5957                         'SET_CIRC_CLAIMS_RETURNED.override',
5958                         'SET_CIRC_LOST',
5959                         'SET_CIRC_MISSING',
5960                         'UNBAR_PATRON',
5961                         'UPDATE_BILL_NOTE',
5962                         'UPDATE_NON_CAT_TYPE',
5963                         'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
5964                         'UPDATE_PATRON_CLAIM_RETURN_COUNT',
5965                         'UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF',
5966                         'UPDATE_PICKUP_LIB_FROM_TRANSIT',
5967                         'UPDATE_USER',
5968                         'VIEW_REPORT_OUTPUT',
5969                         'VIEW_STANDING_PENALTY',
5970                         'VOID_BILLING',
5971                         'VOLUME_HOLDS'
5972                 ) AND NOT EXISTS (
5973                         SELECT 1
5974                         FROM permission.grp_perm_map AS map
5975                         WHERE
5976                                 map.grp = pgt.id
5977                                 AND map.perm = perm.id
5978                 );
5979
5980 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5981         SELECT
5982                 pgt.id, perm.id, aout.depth, TRUE
5983         FROM
5984                 permission.grp_tree pgt,
5985                 permission.perm_list perm,
5986                 actor.org_unit_type aout
5987         WHERE
5988                 pgt.name = 'Local Administrator' AND
5989                 aout.name = 'Branch' AND
5990                 perm.code IN (
5991                         'EVERYTHING'
5992                 ) AND NOT EXISTS (
5993                         SELECT 1
5994                         FROM permission.grp_perm_map AS map
5995                         WHERE
5996                                 map.grp = pgt.id
5997                                 AND map.perm = perm.id
5998                 );
5999
6000 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6001         SELECT
6002                 pgt.id, perm.id, aout.depth, FALSE
6003         FROM
6004                 permission.grp_tree pgt,
6005                 permission.perm_list perm,
6006                 actor.org_unit_type aout
6007         WHERE
6008                 pgt.name = 'Serials' AND
6009                 aout.name = 'System' AND
6010                 perm.code IN (
6011                         'ADMIN_ASSET_COPY_TEMPLATE',
6012                         'ADMIN_SERIAL_CAPTION_PATTERN',
6013                         'ADMIN_SERIAL_DISTRIBUTION',
6014                         'ADMIN_SERIAL_ITEM',
6015                         'ADMIN_SERIAL_STREAM',
6016                         'ADMIN_SERIAL_SUBSCRIPTION',
6017                         'ISSUANCE_HOLDS',
6018                         'RECEIVE_SERIAL'
6019                 ) AND NOT EXISTS (
6020                         SELECT 1
6021                         FROM permission.grp_perm_map AS map
6022                         WHERE
6023                                 map.grp = pgt.id
6024                                 AND map.perm = perm.id
6025                 );
6026
6027 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6028         SELECT
6029                 pgt.id, perm.id, aout.depth, TRUE
6030         FROM
6031                 permission.grp_tree pgt,
6032                 permission.perm_list perm,
6033                 actor.org_unit_type aout
6034         WHERE
6035                 pgt.name = 'System Administrator' AND
6036                 aout.name = 'System' AND
6037                 perm.code IN (
6038                         'EVERYTHING'
6039                 ) AND NOT EXISTS (
6040                         SELECT 1
6041                         FROM permission.grp_perm_map AS map
6042                         WHERE
6043                                 map.grp = pgt.id
6044                                 AND map.perm = perm.id
6045                 );
6046
6047 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6048         SELECT
6049                 pgt.id, perm.id, aout.depth, FALSE
6050         FROM
6051                 permission.grp_tree pgt,
6052                 permission.perm_list perm,
6053                 actor.org_unit_type aout
6054         WHERE
6055                 pgt.name = 'System Administrator' AND
6056                 aout.name = 'Consortium' AND
6057                 perm.code ~ '^VIEW_TRIGGER'
6058                 AND NOT EXISTS (
6059                         SELECT 1
6060                         FROM permission.grp_perm_map AS map
6061                         WHERE
6062                                 map.grp = pgt.id
6063                                 AND map.perm = perm.id
6064                 );
6065
6066 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6067         SELECT
6068                 pgt.id, perm.id, aout.depth, TRUE
6069         FROM
6070                 permission.grp_tree pgt,
6071                 permission.perm_list perm,
6072                 actor.org_unit_type aout
6073         WHERE
6074                 pgt.name = 'Global Administrator' AND
6075                 aout.name = 'Consortium' AND
6076                 perm.code IN (
6077                         'EVERYTHING'
6078                 ) AND NOT EXISTS (
6079                         SELECT 1
6080                         FROM permission.grp_perm_map AS map
6081                         WHERE
6082                                 map.grp = pgt.id
6083                                 AND map.perm = perm.id
6084                 );
6085
6086 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6087         SELECT
6088                 pgt.id, perm.id, aout.depth, FALSE
6089         FROM
6090                 permission.grp_tree pgt,
6091                 permission.perm_list perm,
6092                 actor.org_unit_type aout
6093         WHERE
6094                 pgt.name = 'Data Review' AND
6095                 aout.name = 'Consortium' AND
6096                 perm.code IN (
6097                         'CREATE_COPY_TRANSIT',
6098                         'VIEW_BILLING_TYPE',
6099                         'VIEW_CIRCULATIONS',
6100                         'VIEW_COPY_NOTES',
6101                         'VIEW_HOLD',
6102                         'VIEW_ORG_SETTINGS',
6103                         'VIEW_TITLE_NOTES',
6104                         'VIEW_TRANSACTION',
6105                         'VIEW_USER',
6106                         'VIEW_USER_FINES_SUMMARY',
6107                         'VIEW_USER_TRANSACTIONS',
6108                         'VIEW_VOLUME_NOTES',
6109                         'VIEW_ZIP_DATA'
6110                 ) AND NOT EXISTS (
6111                         SELECT 1
6112                         FROM permission.grp_perm_map AS map
6113                         WHERE
6114                                 map.grp = pgt.id
6115                                 AND map.perm = perm.id
6116                 );
6117
6118 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6119         SELECT
6120                 pgt.id, perm.id, aout.depth, FALSE
6121         FROM
6122                 permission.grp_tree pgt,
6123                 permission.perm_list perm,
6124                 actor.org_unit_type aout
6125         WHERE
6126                 pgt.name = 'Data Review' AND
6127                 aout.name = 'System' AND
6128                 perm.code IN (
6129                         'COPY_CHECKOUT',
6130                         'COPY_HOLDS',
6131                         'CREATE_IN_HOUSE_USE',
6132                         'CREATE_TRANSACTION',
6133                         'OFFLINE_EXECUTE',
6134                         'OFFLINE_VIEW',
6135                         'STAFF_LOGIN',
6136                         'VOLUME_HOLDS'
6137                 ) AND NOT EXISTS (
6138                         SELECT 1
6139                         FROM permission.grp_perm_map AS map
6140                         WHERE
6141                                 map.grp = pgt.id
6142                                 AND map.perm = perm.id
6143                 );
6144
6145 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6146         SELECT
6147                 pgt.id, perm.id, aout.depth, FALSE
6148         FROM
6149                 permission.grp_tree pgt,
6150                 permission.perm_list perm,
6151                 actor.org_unit_type aout
6152         WHERE
6153                 pgt.name = 'Volunteers' AND
6154                 aout.name = 'Branch' AND
6155                 perm.code IN (
6156                         'COPY_CHECKOUT',
6157                         'CREATE_BILL',
6158                         'CREATE_IN_HOUSE_USE',
6159                         'CREATE_PAYMENT',
6160                         'VIEW_BILLING_TYPE',
6161                         'VIEW_CIRCS',
6162                         'VIEW_COPY_CHECKOUT',
6163                         'VIEW_HOLD',
6164                         'VIEW_TITLE_HOLDS',
6165                         'VIEW_TRANSACTION',
6166                         'VIEW_USER',
6167                         'VIEW_USER_FINES_SUMMARY',
6168                         'VIEW_USER_TRANSACTIONS'
6169                 ) AND NOT EXISTS (
6170                         SELECT 1
6171                         FROM permission.grp_perm_map AS map
6172                         WHERE
6173                                 map.grp = pgt.id
6174                                 AND map.perm = perm.id
6175                 );
6176
6177 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6178         SELECT
6179                 pgt.id, perm.id, aout.depth, FALSE
6180         FROM
6181                 permission.grp_tree pgt,
6182                 permission.perm_list perm,
6183                 actor.org_unit_type aout
6184         WHERE
6185                 pgt.name = 'Volunteers' AND
6186                 aout.name = 'Consortium' AND
6187                 perm.code IN (
6188                         'CREATE_COPY_TRANSIT',
6189                         'CREATE_TRANSACTION',
6190                         'CREATE_TRANSIT',
6191                         'STAFF_LOGIN',
6192                         'TRANSIT_COPY',
6193                         'VIEW_ORG_SETTINGS'
6194                 ) AND NOT EXISTS (
6195                         SELECT 1
6196                         FROM permission.grp_perm_map AS map
6197                         WHERE
6198                                 map.grp = pgt.id
6199                                 AND map.perm = perm.id
6200                 );
6201
6202
6203 -- stock Users group
6204 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6205         SELECT
6206                 pgt.id, perm.id, aout.depth, FALSE
6207         FROM
6208                 permission.grp_tree pgt,
6209                 permission.perm_list perm,
6210                 actor.org_unit_type aout
6211         WHERE
6212                 pgt.name = 'Users' AND
6213                 aout.name = 'Consortium' AND
6214                 perm.code IN (
6215                         'CREATE_PURCHASE_REQUEST'
6216                 ) AND NOT EXISTS (
6217                         SELECT 1
6218                         FROM permission.grp_perm_map AS map
6219                         WHERE
6220                                 map.grp = pgt.id
6221                                 AND map.perm = perm.id
6222                 );
6223
6224 -- stock Staff group
6225 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6226         SELECT
6227                 pgt.id, perm.id, aout.depth, FALSE
6228         FROM
6229                 permission.grp_tree pgt,
6230                 permission.perm_list perm,
6231                 actor.org_unit_type aout
6232         WHERE
6233                 pgt.name = 'Staff' AND
6234                 aout.name = 'Consortium' AND
6235                 perm.code IN (
6236                         'VIEW_USER_SETTING_TYPE'
6237                 ) AND NOT EXISTS (
6238                         SELECT 1
6239                         FROM permission.grp_perm_map AS map
6240                         WHERE
6241                                 map.grp = pgt.id
6242                                 AND map.perm = perm.id
6243                 );
6244
6245 -- stock Circulators group
6246 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6247         SELECT
6248                 pgt.id, perm.id, aout.depth, FALSE
6249         FROM
6250                 permission.grp_tree pgt,
6251                 permission.perm_list perm,
6252                 actor.org_unit_type aout
6253         WHERE
6254                 pgt.name = 'Circulators' AND
6255                 aout.name = 'Branch' AND
6256                 perm.code IN (
6257                         'MARK_ITEM_MISSING_PIECES'
6258                 ) AND NOT EXISTS (
6259                         SELECT 1
6260                         FROM permission.grp_perm_map AS map
6261                         WHERE
6262                                 map.grp = pgt.id
6263                                 AND map.perm = perm.id
6264                 );
6265
6266 -- stock Catalogers group
6267 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6268         SELECT
6269                 pgt.id, perm.id, aout.depth, FALSE
6270         FROM
6271                 permission.grp_tree pgt,
6272                 permission.perm_list perm,
6273                 actor.org_unit_type aout
6274         WHERE
6275                 pgt.name = 'Catalogers' AND
6276                 aout.name = 'System' AND
6277                 perm.code IN (
6278                         'MAP_MONOGRAPH_PART'
6279                 ) AND NOT EXISTS (
6280                         SELECT 1
6281                         FROM permission.grp_perm_map AS map
6282                         WHERE
6283                                 map.grp = pgt.id
6284                                 AND map.perm = perm.id
6285                 );
6286
6287 -- stock Acquisitions group
6288 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6289         SELECT
6290                 pgt.id, perm.id, aout.depth, FALSE
6291         FROM
6292                 permission.grp_tree pgt,
6293                 permission.perm_list perm,
6294                 actor.org_unit_type aout
6295         WHERE
6296                 pgt.name = 'Acquisitions' AND
6297                 aout.name = 'Consortium' AND
6298                 perm.code IN (
6299                         'UPDATE_PICKLIST'
6300                 ) AND NOT EXISTS (
6301                         SELECT 1
6302                         FROM permission.grp_perm_map AS map
6303                         WHERE
6304                                 map.grp = pgt.id
6305                                 AND map.perm = perm.id
6306                 );
6307
6308 -- stock Acq Admin group
6309 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6310         SELECT
6311                 pgt.id, perm.id, aout.depth, TRUE
6312         FROM
6313                 permission.grp_tree pgt,
6314                 permission.perm_list perm,
6315                 actor.org_unit_type aout
6316         WHERE
6317                 pgt.name = 'Acquisitions Administrator' AND
6318                 aout.name = 'Consortium' AND
6319                 perm.code IN (
6320                         'UPDATE_PICKLIST'
6321                 ) AND NOT EXISTS (
6322                         SELECT 1
6323                         FROM permission.grp_perm_map AS map
6324                         WHERE
6325                                 map.grp = pgt.id
6326                                 AND map.perm = perm.id
6327                 );
6328
6329 INSERT INTO config.upgrade_log (version) VALUES ('0547'); -- dbwells
6330
6331 -- account for spelling errors (Admin != Administrator)
6332 \qecho This might not insert much if you passed through 0542 on your way here,
6333 \qecho but one group was missed there as well
6334
6335 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6336         SELECT
6337                 pgt.id, perm.id, aout.depth, TRUE
6338         FROM
6339                 permission.grp_tree pgt,
6340                 permission.perm_list perm,
6341                 actor.org_unit_type aout
6342         WHERE
6343                 pgt.name = 'Cataloging Administrator' AND
6344                 aout.name = 'Consortium' AND
6345                 perm.code IN (
6346                         'ADMIN_IMPORT_ITEM_ATTR_DEF',
6347                         'ADMIN_MERGE_PROFILE',
6348                         'CREATE_AUTHORITY_IMPORT_IMPORT_DEF',
6349                         'CREATE_BIB_IMPORT_FIELD_DEF',
6350                         'CREATE_BIB_PTYPE',
6351                         'CREATE_BIB_SOURCE',
6352                         'CREATE_IMPORT_ITEM_ATTR_DEF',
6353                         'CREATE_IMPORT_TRASH_FIELD',
6354                         'CREATE_MERGE_PROFILE',
6355                         'CREATE_MONOGRAPH_PART',
6356                         'CREATE_VOLUME_PREFIX',
6357                         'CREATE_VOLUME_SUFFIX',
6358                         'DELETE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
6359                         'DELETE_BIB_PTYPE',
6360                         'DELETE_BIB_SOURCE',
6361                         'DELETE_IMPORT_ITEM_ATTR_DEF',
6362                         'DELETE_IMPORT_TRASH_FIELD',
6363                         'DELETE_MERGE_PROFILE',
6364                         'DELETE_MONOGRAPH_PART',
6365                         'DELETE_VOLUME_PREFIX',
6366                         'DELETE_VOLUME_SUFFIX',
6367                         'MAP_MONOGRAPH_PART',
6368                         'UPDATE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
6369                         'UPDATE_BIB_IMPORT_IMPORT_FIELD_DEF',
6370                         'UPDATE_BIB_PTYPE',
6371                         'UPDATE_IMPORT_ITEM_ATTR_DEF',
6372                         'UPDATE_IMPORT_TRASH_FIELD',
6373                         'UPDATE_MERGE_PROFILE',
6374                         'UPDATE_MONOGRAPH_PART',
6375                         'UPDATE_VOLUME_PREFIX',
6376                         'UPDATE_VOLUME_SUFFIX'
6377                 ) AND NOT EXISTS (
6378                         SELECT 1
6379                         FROM permission.grp_perm_map AS map
6380                         WHERE
6381                                 map.grp = pgt.id
6382                                 AND map.perm = perm.id
6383                 );
6384
6385 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6386     SELECT
6387         pgt.id, perm.id, aout.depth, TRUE
6388     FROM
6389         permission.grp_tree pgt,
6390         permission.perm_list perm,
6391         actor.org_unit_type aout
6392     WHERE
6393         pgt.name = 'Cataloging Administrator' AND
6394         aout.name = 'System' AND
6395         perm.code IN (
6396             'CREATE_COPY_STAT_CAT',
6397             'CREATE_COPY_STAT_CAT_ENTRY',
6398             'CREATE_COPY_STAT_CAT_ENTRY_MAP',
6399             'RUN_REPORTS',
6400             'SHARE_REPORT_FOLDER',
6401             'UPDATE_COPY_LOCATION',
6402             'UPDATE_COPY_STAT_CAT',
6403             'UPDATE_COPY_STAT_CAT_ENTRY',
6404             'VIEW_REPORT_OUTPUT'
6405         ) AND NOT EXISTS (
6406             SELECT 1
6407             FROM permission.grp_perm_map AS map
6408             WHERE
6409                 map.grp = pgt.id
6410                 AND map.perm = perm.id
6411         );
6412
6413 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6414         SELECT
6415                 pgt.id, perm.id, aout.depth, TRUE
6416         FROM
6417                 permission.grp_tree pgt,
6418                 permission.perm_list perm,
6419                 actor.org_unit_type aout
6420         WHERE
6421                 pgt.name = 'Circulation Administrator' AND
6422                 aout.name = 'Branch' AND
6423                 perm.code IN (
6424                         'DELETE_USER'
6425                 ) AND NOT EXISTS (
6426                         SELECT 1
6427                         FROM permission.grp_perm_map AS map
6428                         WHERE
6429                                 map.grp = pgt.id
6430                                 AND map.perm = perm.id
6431                 );
6432
6433 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6434         SELECT
6435                 pgt.id, perm.id, aout.depth, TRUE
6436         FROM
6437                 permission.grp_tree pgt,
6438                 permission.perm_list perm,
6439                 actor.org_unit_type aout
6440         WHERE
6441                 pgt.name = 'Circulation Administrator' AND
6442                 aout.name = 'Consortium' AND
6443                 perm.code IN (
6444                         'ADMIN_MAX_FINE_RULE',
6445                         'CREATE_CIRC_DURATION',
6446                         'DELETE_CIRC_DURATION',
6447                         'MARK_ITEM_MISSING_PIECES',
6448                         'UPDATE_CIRC_DURATION',
6449                         'UPDATE_HOLD_REQUEST_TIME',
6450                         'UPDATE_NET_ACCESS_LEVEL',
6451                         'VIEW_CIRC_MATRIX_MATCHPOINT',
6452                         'VIEW_HOLD_MATRIX_MATCHPOINT'
6453                 ) AND NOT EXISTS (
6454                         SELECT 1
6455                         FROM permission.grp_perm_map AS map
6456                         WHERE
6457                                 map.grp = pgt.id
6458                                 AND map.perm = perm.id
6459                 );
6460
6461 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6462         SELECT
6463                 pgt.id, perm.id, aout.depth, TRUE
6464         FROM
6465                 permission.grp_tree pgt,
6466                 permission.perm_list perm,
6467                 actor.org_unit_type aout
6468         WHERE
6469                 pgt.name = 'Circulation Administrator' AND
6470                 aout.name = 'System' AND
6471                 perm.code IN (
6472                         'ADMIN_BOOKING_RESERVATION',
6473                         'ADMIN_BOOKING_RESERVATION_ATTR_MAP',
6474                         'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP',
6475                         'ADMIN_BOOKING_RESOURCE',
6476                         'ADMIN_BOOKING_RESOURCE_ATTR',
6477                         'ADMIN_BOOKING_RESOURCE_ATTR_MAP',
6478                         'ADMIN_BOOKING_RESOURCE_ATTR_VALUE',
6479                         'ADMIN_BOOKING_RESOURCE_TYPE',
6480                         'ADMIN_COPY_LOCATION_ORDER',
6481                         'ADMIN_HOLD_CANCEL_CAUSE',
6482                         'ASSIGN_GROUP_PERM',
6483                         'BAR_PATRON',
6484                         'COPY_HOLDS',
6485                         'COPY_TRANSIT_RECEIVE',
6486                         'CREATE_BILL',
6487                         'CREATE_BILLING_TYPE',
6488                         'CREATE_NON_CAT_TYPE',
6489                         'CREATE_PATRON_STAT_CAT',
6490                         'CREATE_PATRON_STAT_CAT_ENTRY',
6491                         'CREATE_PATRON_STAT_CAT_ENTRY_MAP',
6492                         'CREATE_USER_GROUP_LINK',
6493                         'DELETE_BILLING_TYPE',
6494                         'DELETE_NON_CAT_TYPE',
6495                         'DELETE_PATRON_STAT_CAT',
6496                         'DELETE_PATRON_STAT_CAT_ENTRY',
6497                         'DELETE_PATRON_STAT_CAT_ENTRY_MAP',
6498                         'DELETE_TRANSIT',
6499                         'group_application.user.staff',
6500                         'MANAGE_BAD_DEBT',
6501                         'MARK_ITEM_AVAILABLE',
6502                         'MARK_ITEM_BINDERY',
6503                         'MARK_ITEM_CHECKED_OUT',
6504                         'MARK_ITEM_ILL',
6505                         'MARK_ITEM_IN_PROCESS',
6506                         'MARK_ITEM_IN_TRANSIT',
6507                         'MARK_ITEM_LOST',
6508                         'MARK_ITEM_MISSING',
6509                         'MARK_ITEM_ON_HOLDS_SHELF',
6510                         'MARK_ITEM_ON_ORDER',
6511                         'MARK_ITEM_RESHELVING',
6512                         'MERGE_USERS',
6513                         'money.collections_tracker.create',
6514                         'money.collections_tracker.delete',
6515                         'OFFLINE_EXECUTE',
6516                         'OFFLINE_UPLOAD',
6517                         'OFFLINE_VIEW',
6518                         'REMOVE_USER_GROUP_LINK',
6519                         'SET_CIRC_CLAIMS_RETURNED',
6520                         'SET_CIRC_CLAIMS_RETURNED.override',
6521                         'SET_CIRC_LOST',
6522                         'SET_CIRC_MISSING',
6523                         'UNBAR_PATRON',
6524                         'UPDATE_BILL_NOTE',
6525                         'UPDATE_NON_CAT_TYPE',
6526                         'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
6527                         'UPDATE_PATRON_CLAIM_RETURN_COUNT',
6528                         'UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF',
6529                         'UPDATE_PICKUP_LIB_FROM_TRANSIT',
6530                         'UPDATE_USER',
6531                         'VIEW_REPORT_OUTPUT',
6532                         'VIEW_STANDING_PENALTY',
6533                         'VOID_BILLING',
6534                         'VOLUME_HOLDS'
6535                 ) AND NOT EXISTS (
6536                         SELECT 1
6537                         FROM permission.grp_perm_map AS map
6538                         WHERE
6539                                 map.grp = pgt.id
6540                                 AND map.perm = perm.id
6541                 );
6542
6543 INSERT INTO config.upgrade_log (version) VALUES ('0557'); -- miker
6544
6545 CREATE OR REPLACE FUNCTION unapi.acl ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
6546     SELECT  XMLELEMENT(
6547                 name location,
6548                 XMLATTRIBUTES(
6549                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6550                     id AS ident,
6551                     holdable,
6552                     opac_visible,
6553                     label_prefix AS prefix,
6554                     label_suffix AS suffix
6555                 ),
6556                 name
6557             )
6558       FROM  asset.copy_location
6559       WHERE id = $1;
6560 $F$ LANGUAGE SQL;
6561
6562 INSERT INTO config.upgrade_log (version) VALUES ('0558'); -- miker
6563
6564 CREATE OR REPLACE FUNCTION unapi.ccs ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
6565     SELECT  XMLELEMENT(
6566                 name status,
6567                 XMLATTRIBUTES(
6568                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6569                     id AS ident,
6570                     holdable,
6571                     opac_visible
6572                 ),
6573                 name
6574             )
6575       FROM  config.copy_status
6576       WHERE id = $1;
6577 $F$ LANGUAGE SQL;
6578
6579 INSERT INTO config.upgrade_log (version) VALUES ('0560'); -- miker
6580
6581 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
6582 DECLARE
6583     add_query       TEXT;
6584     remove_query    TEXT;
6585     do_add          BOOLEAN := false;
6586     do_remove       BOOLEAN := false;
6587 BEGIN
6588     add_query := $$
6589             INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
6590               SELECT id, circ_lib, record FROM (
6591                 SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
6592                   FROM  asset.copy cp
6593                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
6594                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
6595                         JOIN asset.copy_location cl ON (cp.location = cl.id)
6596                         JOIN config.copy_status cs ON (cp.status = cs.id)
6597                         JOIN biblio.record_entry b ON (cn.record = b.id)
6598                   WHERE NOT cp.deleted
6599                         AND NOT cn.deleted
6600                         AND NOT b.deleted
6601                         AND cs.opac_visible
6602                         AND cl.opac_visible
6603                         AND cp.opac_visible
6604                         AND a.opac_visible
6605                             UNION
6606                 SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
6607                   FROM  asset.copy cp
6608                         JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
6609                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
6610                         JOIN asset.copy_location cl ON (cp.location = cl.id)
6611                         JOIN config.copy_status cs ON (cp.status = cs.id)
6612                   WHERE NOT cp.deleted
6613                         AND cs.opac_visible
6614                         AND cl.opac_visible
6615                         AND cp.opac_visible
6616                         AND a.opac_visible
6617                     ) AS x 
6618
6619     $$;
6620  
6621     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
6622
6623     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
6624         IF TG_OP = 'INSERT' THEN
6625             add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
6626             EXECUTE add_query;
6627             RETURN NEW;
6628         ELSE
6629             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
6630             EXECUTE remove_query;
6631             RETURN OLD;
6632         END IF;
6633     END IF;
6634
6635     IF TG_OP = 'INSERT' THEN
6636
6637         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
6638             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
6639             EXECUTE add_query;
6640         END IF;
6641
6642         RETURN NEW;
6643
6644     END IF;
6645
6646     -- handle items first, since with circulation activity
6647     -- their statuses change frequently
6648     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
6649
6650         IF OLD.location    <> NEW.location OR
6651            OLD.call_number <> NEW.call_number OR
6652            OLD.status      <> NEW.status OR
6653            OLD.circ_lib    <> NEW.circ_lib THEN
6654             -- any of these could change visibility, but
6655             -- we'll save some queries and not try to calculate
6656             -- the change directly
6657             do_remove := true;
6658             do_add := true;
6659         ELSE
6660
6661             IF OLD.deleted <> NEW.deleted THEN
6662                 IF NEW.deleted THEN
6663                     do_remove := true;
6664                 ELSE
6665                     do_add := true;
6666                 END IF;
6667             END IF;
6668
6669             IF OLD.opac_visible <> NEW.opac_visible THEN
6670                 IF OLD.opac_visible THEN
6671                     do_remove := true;
6672                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
6673                                         -- is also marked opac_visible
6674                     do_add := true;
6675                 END IF;
6676             END IF;
6677
6678         END IF;
6679
6680         IF do_remove THEN
6681             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
6682         END IF;
6683         IF do_add THEN
6684             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
6685             EXECUTE add_query;
6686         END IF;
6687
6688         RETURN NEW;
6689
6690     END IF;
6691
6692     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
6693  
6694         IF OLD.deleted AND NEW.deleted THEN -- do nothing
6695
6696             RETURN NEW;
6697  
6698         ELSIF NEW.deleted THEN -- remove rows
6699  
6700             IF TG_TABLE_NAME = 'call_number' THEN
6701                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
6702             ELSIF TG_TABLE_NAME = 'record_entry' THEN
6703                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
6704             END IF;
6705  
6706             RETURN NEW;
6707  
6708         ELSIF OLD.deleted THEN -- add rows
6709  
6710             IF TG_TABLE_NAME IN ('copy','unit') THEN
6711                 add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
6712             ELSIF TG_TABLE_NAME = 'call_number' THEN
6713                 add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
6714             ELSIF TG_TABLE_NAME = 'record_entry' THEN
6715                 add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
6716             END IF;
6717  
6718             EXECUTE add_query;
6719             RETURN NEW;
6720  
6721         END IF;
6722  
6723     END IF;
6724
6725     IF TG_TABLE_NAME = 'call_number' THEN
6726
6727         IF OLD.record <> NEW.record THEN
6728             -- call number is linked to different bib
6729             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
6730             EXECUTE remove_query;
6731             add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
6732             EXECUTE add_query;
6733         END IF;
6734
6735         RETURN NEW;
6736
6737     END IF;
6738
6739     IF TG_TABLE_NAME IN ('record_entry') THEN
6740         RETURN NEW; -- don't have 'opac_visible'
6741     END IF;
6742
6743     -- actor.org_unit, asset.copy_location, asset.copy_status
6744     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
6745
6746         RETURN NEW;
6747
6748     ELSIF NEW.opac_visible THEN -- add rows
6749
6750         IF TG_TABLE_NAME = 'org_unit' THEN
6751             add_query := add_query || 'WHERE x.circ_lib = ' || NEW.id || ';';
6752         ELSIF TG_TABLE_NAME = 'copy_location' THEN
6753             add_query := add_query || 'WHERE x.location = ' || NEW.id || ';';
6754         ELSIF TG_TABLE_NAME = 'copy_status' THEN
6755             add_query := add_query || 'WHERE x.status = ' || NEW.id || ';';
6756         END IF;
6757  
6758         EXECUTE add_query;
6759  
6760     ELSE -- delete rows
6761
6762         IF TG_TABLE_NAME = 'org_unit' THEN
6763             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
6764         ELSIF TG_TABLE_NAME = 'copy_location' THEN
6765             remove_query := remove_query || 'location = ' || NEW.id || ');';
6766         ELSIF TG_TABLE_NAME = 'copy_status' THEN
6767             remove_query := remove_query || 'status = ' || NEW.id || ');';
6768         END IF;
6769  
6770         EXECUTE remove_query;
6771  
6772     END IF;
6773  
6774     RETURN NEW;
6775 END;
6776 $func$ LANGUAGE PLPGSQL;
6777
6778 INSERT INTO config.upgrade_log (version) VALUES ('0563');
6779
6780 INSERT INTO permission.perm_list ( id, code, description ) 
6781     VALUES ( 510, 'UPDATE_PATRON_COLLECTIONS_EXEMPT', oils_i18n_gettext(510,
6782     'Allows a user to indicate that a patron is exempt from collections processing', 'ppl', 'description'));
6783
6784 --- stock Circulation Administrator group
6785
6786 INSERT INTO permission.grp_perm_map ( grp, perm, depth, grantable )
6787     SELECT
6788         4,
6789         id,
6790         0,
6791         't'
6792     FROM permission.perm_list
6793     WHERE code in ('UPDATE_PATRON_COLLECTIONS_EXEMPT');
6794
6795 INSERT INTO config.upgrade_log (version) VALUES ('0566');
6796
6797 CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
6798 DECLARE
6799     me      biblio.record_entry%ROWTYPE;
6800     layout  unapi.bre_output_layout%ROWTYPE;
6801     xfrm    config.xml_transform%ROWTYPE;
6802     ouid    INT;
6803     tmp_xml TEXT;
6804     top_el  TEXT;
6805     output  XML;
6806     hxml    XML;
6807     axml    XML;
6808 BEGIN
6809
6810     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
6811
6812     IF ouid IS NULL THEN
6813         RETURN NULL::XML;
6814     END IF;
6815
6816     IF format = 'holdings_xml' THEN -- the special case
6817         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
6818         RETURN output;
6819     END IF;
6820
6821     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
6822
6823     IF layout.name IS NULL THEN
6824         RETURN NULL::XML;
6825     END IF;
6826
6827     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
6828
6829     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
6830
6831     -- grab SVF if we need them
6832     IF ('mra' = ANY (includes)) THEN 
6833         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
6834     ELSE
6835         axml := NULL::XML;
6836     END IF;
6837
6838     -- grab hodlings if we need them
6839     IF ('holdings_xml' = ANY (includes)) THEN 
6840         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
6841     ELSE
6842         hxml := NULL::XML;
6843     END IF;
6844
6845
6846     -- generate our item node
6847
6848
6849     IF format = 'marcxml' THEN
6850         tmp_xml := me.marc;
6851         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
6852            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
6853         END IF; 
6854     ELSE
6855         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
6856     END IF;
6857
6858     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
6859
6860     IF axml IS NOT NULL THEN 
6861         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
6862     END IF;
6863
6864     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
6865         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
6866     END IF;
6867
6868     IF ('bre.unapi' = ANY (includes)) THEN 
6869         output := REGEXP_REPLACE(
6870             tmp_xml,
6871             '</' || top_el || '>(.*?)',
6872             XMLELEMENT(
6873                 name abbr,
6874                 XMLATTRIBUTES(
6875                     'http://www.w3.org/1999/xhtml' AS xmlns,
6876                     'unapi-id' AS class,
6877                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
6878                 )
6879             )::TEXT || '</' || top_el || E'>\\1'
6880         );
6881     ELSE
6882         output := tmp_xml;
6883     END IF;
6884
6885     output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
6886     RETURN output;
6887 END;
6888 $F$ LANGUAGE PLPGSQL;
6889
6890 CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$
6891      SELECT  XMLELEMENT(
6892                  name holdings,
6893                  XMLATTRIBUTES(
6894                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6895                     CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
6896                  ),
6897                  XMLELEMENT(
6898                      name counts,
6899                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
6900                          SELECT  XMLELEMENT(
6901                                      name count,
6902                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
6903                                  )::text
6904                            FROM  asset.opac_ou_record_copy_count($2,  $1)
6905                                      UNION
6906                          SELECT  XMLELEMENT(
6907                                      name count,
6908                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
6909                                  )::text
6910                            FROM  asset.staff_ou_record_copy_count($2, $1)
6911                                      ORDER BY 1
6912                      )x)
6913                  ),
6914                  CASE 
6915                      WHEN ('bmp' = ANY ($5)) THEN
6916                         XMLELEMENT(
6917                             name monograph_parts,
6918                             (SELECT XMLAGG(bmp) FROM (
6919                                 SELECT  unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE)
6920                                   FROM  biblio.monograph_part
6921                                   WHERE record = $1
6922                             )x)
6923                         )
6924                      ELSE NULL
6925                  END,
6926                  XMLELEMENT(
6927                      name volumes,
6928                      (SELECT XMLAGG(acn) FROM (
6929                         SELECT  unapi.acn(acn.id,'xml','volume', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
6930                           FROM  asset.call_number acn
6931                           WHERE acn.record = $1
6932                                 AND EXISTS (
6933                                     SELECT  1
6934                                       FROM  asset.copy acp
6935                                             JOIN actor.org_unit_descendants(
6936                                                 $2,
6937                                                 (COALESCE(
6938                                                     $4,
6939                                                     (SELECT aout.depth
6940                                                       FROM  actor.org_unit_type aout
6941                                                             JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
6942                                                     )
6943                                                 ))
6944                                             ) aoud ON (acp.circ_lib = aoud.id)
6945                                       LIMIT 1
6946                                )
6947                           ORDER BY label_sortkey
6948                           LIMIT $6
6949                           OFFSET $7
6950                      )x)
6951                  ),
6952                  CASE WHEN ('ssub' = ANY ($5)) THEN 
6953                      XMLELEMENT(
6954                          name subscriptions,
6955                          (SELECT XMLAGG(ssub) FROM (
6956                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
6957                               FROM  serial.subscription
6958                               WHERE record_entry = $1
6959                         )x)
6960                      )
6961                  ELSE NULL END,
6962                  CASE WHEN ('acp' = ANY ($5)) THEN 
6963                      XMLELEMENT(
6964                          name foreign_copies,
6965                          (SELECT XMLAGG(acp) FROM (
6966                             SELECT  unapi.acp(p.target_copy,'xml','copy','{}'::TEXT[], $3, $4, $6, $7, FALSE)
6967                               FROM  biblio.peer_bib_copy_map p
6968                                     JOIN asset.copy c ON (p.target_copy = c.id)
6969                               WHERE NOT c.deleted AND peer_record = $1
6970                         )x)
6971                      )
6972                  ELSE NULL END
6973              );
6974 $F$ LANGUAGE SQL;
6975
6976 CREATE OR REPLACE FUNCTION unapi.ssub ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
6977         SELECT  XMLELEMENT(
6978                     name subscription,
6979                     XMLATTRIBUTES(
6980                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6981                         'tag:open-ils.org:U2@ssub/' || id AS id,
6982                         start_date AS start, end_date AS end, expected_date_offset
6983                     ),
6984                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8),
6985                     XMLELEMENT( name distributions,
6986                         CASE 
6987                             WHEN ('sdist' = ANY ($4)) THEN
6988                                 (SELECT XMLAGG(sdist) FROM (
6989                                     SELECT  unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE)
6990                                       FROM  serial.distribution
6991                                       WHERE subscription = ssub.id
6992                                 )x)
6993                             ELSE NULL
6994                         END
6995                     )
6996                 )
6997           FROM  serial.subscription ssub
6998           WHERE id = $1
6999           GROUP BY id, start_date, end_date, expected_date_offset, owning_lib;
7000 $F$ LANGUAGE SQL;
7001
7002 CREATE OR REPLACE FUNCTION unapi.sdist ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7003         SELECT  XMLELEMENT(
7004                     name distribution,
7005                     XMLATTRIBUTES(
7006                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7007                         'tag:open-ils.org:U2@sdist/' || id AS id,
7008                         'tag:open-ils.org:U2@acn/' || receive_call_number AS receive_call_number,
7009                         'tag:open-ils.org:U2@acn/' || bind_call_number AS bind_call_number,
7010                         unit_label_prefix, label, unit_label_suffix, summary_method
7011                     ),
7012                     unapi.aou( holding_lib, $2, 'holding_lib', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8),
7013                     CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7014                     XMLELEMENT( name streams,
7015                         CASE 
7016                             WHEN ('sstr' = ANY ($4)) THEN
7017                                 (SELECT XMLAGG(sstr) FROM (
7018                                     SELECT  unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7019                                       FROM  serial.stream
7020                                       WHERE distribution = sdist.id
7021                                 )x)
7022                             ELSE NULL
7023                         END
7024                     ),
7025                     XMLELEMENT( name summaries,
7026                         CASE 
7027                             WHEN ('ssum' = ANY ($4)) THEN
7028                                 (SELECT XMLAGG(sbsum) FROM (
7029                                     SELECT  unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7030                                       FROM  serial.basic_summary
7031                                       WHERE distribution = sdist.id
7032                                 )x)
7033                             ELSE NULL
7034                         END,
7035                         CASE 
7036                             WHEN ('ssum' = ANY ($4)) THEN
7037                                 (SELECT XMLAGG(sisum) FROM (
7038                                     SELECT  unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7039                                       FROM  serial.index_summary
7040                                       WHERE distribution = sdist.id
7041                                 )x)
7042                             ELSE NULL
7043                         END,
7044                         CASE 
7045                             WHEN ('ssum' = ANY ($4)) THEN
7046                                 (SELECT XMLAGG(sssum) FROM (
7047                                     SELECT  unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7048                                       FROM  serial.supplement_summary
7049                                       WHERE distribution = sdist.id
7050                                 )x)
7051                             ELSE NULL
7052                         END
7053                     )
7054                 )
7055           FROM  serial.distribution sdist
7056           WHERE id = $1
7057           GROUP BY id, label, unit_label_prefix, unit_label_suffix, holding_lib, summary_method, subscription, receive_call_number, bind_call_number;
7058 $F$ LANGUAGE SQL;
7059
7060 CREATE OR REPLACE FUNCTION unapi.sstr ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7061     SELECT  XMLELEMENT(
7062                 name stream,
7063                 XMLATTRIBUTES(
7064                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7065                     'tag:open-ils.org:U2@sstr/' || id AS id,
7066                     routing_label
7067                 ),
7068                 CASE WHEN distribution IS NOT NULL AND ('sdist' = ANY ($4)) THEN unapi.sssum( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7069                 XMLELEMENT( name items,
7070                     CASE 
7071                         WHEN ('sitem' = ANY ($4)) THEN
7072                             (SELECT XMLAGG(sitem) FROM (
7073                                 SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE)
7074                                   FROM  serial.item
7075                                   WHERE stream = sstr.id
7076                             )x)
7077                         ELSE NULL
7078                     END
7079                 )
7080             )
7081       FROM  serial.stream sstr
7082       WHERE id = $1
7083       GROUP BY id, routing_label, distribution;
7084 $F$ LANGUAGE SQL;
7085
7086 CREATE OR REPLACE FUNCTION unapi.siss ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7087     SELECT  XMLELEMENT(
7088                 name issuance,
7089                 XMLATTRIBUTES(
7090                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7091                     'tag:open-ils.org:U2@siss/' || id AS id,
7092                     create_date, edit_date, label, date_published,
7093                     holding_code, holding_type, holding_link_id
7094                 ),
7095                 CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7096                 XMLELEMENT( name items,
7097                     CASE 
7098                         WHEN ('sitem' = ANY ($4)) THEN
7099                             (SELECT XMLAGG(sitem) FROM (
7100                                 SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE)
7101                                   FROM  serial.item
7102                                   WHERE issuance = sstr.id
7103                             )x)
7104                         ELSE NULL
7105                     END
7106                 )
7107             )
7108       FROM  serial.issuance sstr
7109       WHERE id = $1
7110       GROUP BY id, create_date, edit_date, label, date_published, holding_code, holding_type, holding_link_id, subscription;
7111 $F$ LANGUAGE SQL;
7112
7113 CREATE OR REPLACE FUNCTION unapi.sitem ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7114         SELECT  XMLELEMENT(
7115                     name serial_item,
7116                     XMLATTRIBUTES(
7117                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7118                         'tag:open-ils.org:U2@sitem/' || id AS id,
7119                         'tag:open-ils.org:U2@siss/' || issuance AS issuance,
7120                         date_expected, date_received
7121                     ),
7122                     CASE WHEN issuance IS NOT NULL AND ('siss' = ANY ($4)) THEN unapi.siss( issuance, $2, 'issuance', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7123                     CASE WHEN stream IS NOT NULL AND ('sstr' = ANY ($4)) THEN unapi.sstr( stream, $2, 'stream', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7124                     CASE WHEN unit IS NOT NULL AND ('sunit' = ANY ($4)) THEN unapi.sunit( stream, $2, 'serial_unit', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7125                     CASE WHEN uri IS NOT NULL AND ('auri' = ANY ($4)) THEN unapi.auri( uri, $2, 'uri', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END
7126 --                    XMLELEMENT( name notes,
7127 --                        CASE 
7128 --                            WHEN ('acpn' = ANY ($4)) THEN
7129 --                                (SELECT XMLAGG(acpn) FROM (
7130 --                                    SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8)
7131 --                                      FROM  asset.copy_note
7132 --                                      WHERE owning_copy = cp.id AND pub
7133 --                                )x)
7134 --                            ELSE NULL
7135 --                        END
7136 --                    )
7137                 )
7138           FROM  serial.item sitem
7139           WHERE id = $1;
7140 $F$ LANGUAGE SQL;
7141
7142
7143 CREATE OR REPLACE FUNCTION unapi.bmp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7144         SELECT  XMLELEMENT(
7145                     name monograph_part,
7146                     XMLATTRIBUTES(
7147                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7148                         'tag:open-ils.org:U2@bmp/' || id AS id,
7149                         id AS ident,
7150                         label,
7151                         label_sortkey,
7152                         'tag:open-ils.org:U2@bre/' || record AS record
7153                     ),
7154                     CASE 
7155                         WHEN ('acp' = ANY ($4)) THEN
7156                             XMLELEMENT( name copies,
7157                                 (SELECT XMLAGG(acp) FROM (
7158                                     SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE)
7159                                       FROM  asset.copy cp
7160                                             JOIN asset.copy_part_map cpm ON (cpm.target_copy = cp.id)
7161                                       WHERE cpm.part = $1
7162                                       ORDER BY COALESCE(cp.copy_number,0), cp.barcode
7163                                       LIMIT $7
7164                                       OFFSET $8
7165                                 )x)
7166                             )
7167                         ELSE NULL
7168                     END,
7169                     CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( record, 'marcxml', 'record', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE) ELSE NULL END
7170                 )
7171           FROM  biblio.monograph_part
7172           WHERE id = $1
7173           GROUP BY id, label, label_sortkey, record;
7174 $F$ LANGUAGE SQL;
7175
7176 CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7177         SELECT  XMLELEMENT(
7178                     name copy,
7179                     XMLATTRIBUTES(
7180                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7181                         'tag:open-ils.org:U2@acp/' || id AS id,
7182                         create_date, edit_date, copy_number, circulate, deposit,
7183                         ref, holdable, deleted, deposit_amount, price, barcode,
7184                         circ_modifier, circ_as_type, opac_visible
7185                     ),
7186                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
7187                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
7188                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
7189                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
7190                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7191                     XMLELEMENT( name copy_notes,
7192                         CASE 
7193                             WHEN ('acpn' = ANY ($4)) THEN
7194                                 (SELECT XMLAGG(acpn) FROM (
7195                                     SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7196                                       FROM  asset.copy_note
7197                                       WHERE owning_copy = cp.id AND pub
7198                                 )x)
7199                             ELSE NULL
7200                         END
7201                     ),
7202                     XMLELEMENT( name statcats,
7203                         CASE 
7204                             WHEN ('ascecm' = ANY ($4)) THEN
7205                                 (SELECT XMLAGG(ascecm) FROM (
7206                                     SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7207                                       FROM  asset.stat_cat_entry_copy_map
7208                                       WHERE owning_copy = cp.id
7209                                 )x)
7210                             ELSE NULL
7211                         END
7212                     ),
7213                     XMLELEMENT( name foreign_records,
7214                         CASE
7215                             WHEN ('bre' = ANY ($4)) THEN
7216                                 (SELECT XMLAGG(bre) FROM (
7217                                     SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
7218                                       FROM  biblio.peer_bib_copy_map
7219                                       WHERE target_copy = cp.id
7220                                 )x)
7221                             ELSE NULL
7222                         END
7223
7224                     ),
7225                     CASE 
7226                         WHEN ('bmp' = ANY ($4)) THEN
7227                             XMLELEMENT( name monograph_parts,
7228                                 (SELECT XMLAGG(bmp) FROM (
7229                                     SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7230                                       FROM  asset.copy_part_map
7231                                       WHERE target_copy = cp.id
7232                                 )x)
7233                             )
7234                         ELSE NULL
7235                     END
7236                 )
7237           FROM  asset.copy cp
7238           WHERE id = $1
7239           GROUP BY id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible;
7240 $F$ LANGUAGE SQL;
7241
7242 CREATE OR REPLACE FUNCTION unapi.sunit ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7243         SELECT  XMLELEMENT(
7244                     name serial_unit,
7245                     XMLATTRIBUTES(
7246                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7247                         'tag:open-ils.org:U2@acp/' || id AS id,
7248                         create_date, edit_date, copy_number, circulate, deposit,
7249                         ref, holdable, deleted, deposit_amount, price, barcode,
7250                         circ_modifier, circ_as_type, opac_visible, status_changed_time,
7251                         floating, mint_condition, detailed_contents, sort_key, summary_contents, cost 
7252                     ),
7253                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
7254                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
7255                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
7256                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
7257                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7258                     XMLELEMENT( name copy_notes,
7259                         CASE 
7260                             WHEN ('acpn' = ANY ($4)) THEN
7261                                 (SELECT XMLAGG(acpn) FROM (
7262                                     SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE)
7263                                       FROM  asset.copy_note
7264                                       WHERE owning_copy = cp.id AND pub
7265                                 )x)
7266                             ELSE NULL
7267                         END
7268                     ),
7269                     XMLELEMENT( name statcats,
7270                         CASE 
7271                             WHEN ('ascecm' = ANY ($4)) THEN
7272                                 (SELECT XMLAGG(ascecm) FROM (
7273                                     SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7274                                       FROM  asset.stat_cat_entry_copy_map
7275                                       WHERE owning_copy = cp.id
7276                                 )x)
7277                             ELSE NULL
7278                         END
7279                     ),
7280                     XMLELEMENT( name foreign_records,
7281                         CASE
7282                             WHEN ('bre' = ANY ($4)) THEN
7283                                 (SELECT XMLAGG(bre) FROM (
7284                                     SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
7285                                       FROM  biblio.peer_bib_copy_map
7286                                       WHERE target_copy = cp.id
7287                                 )x)
7288                             ELSE NULL
7289                         END
7290
7291                     ),
7292                     CASE 
7293                         WHEN ('bmp' = ANY ($4)) THEN
7294                             XMLELEMENT( name monograph_parts,
7295                                 (SELECT XMLAGG(bmp) FROM (
7296                                     SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7297                                       FROM  asset.copy_part_map
7298                                       WHERE target_copy = cp.id
7299                                 )x)
7300                             )
7301                         ELSE NULL
7302                     END
7303                 )
7304           FROM  serial.unit cp
7305           WHERE id = $1
7306           GROUP BY  id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, floating, mint_condition,
7307                     deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible, status_changed_time, detailed_contents, sort_key, summary_contents, cost;
7308 $F$ LANGUAGE SQL;
7309
7310 INSERT INTO config.upgrade_log (version) VALUES ('0568'); -- miker for tsbere
7311
7312 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
7313 DECLARE
7314     add_front       TEXT;
7315     add_back        TEXT;
7316     add_base_query  TEXT;
7317     add_peer_query  TEXT;
7318     remove_query    TEXT;
7319     do_add          BOOLEAN := false;
7320     do_remove       BOOLEAN := false;
7321 BEGIN
7322     add_base_query := $$
7323         SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
7324           FROM  asset.copy cp
7325                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
7326                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7327                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7328                 JOIN config.copy_status cs ON (cp.status = cs.id)
7329                 JOIN biblio.record_entry b ON (cn.record = b.id)
7330           WHERE NOT cp.deleted
7331                 AND NOT cn.deleted
7332                 AND NOT b.deleted
7333                 AND cs.opac_visible
7334                 AND cl.opac_visible
7335                 AND cp.opac_visible
7336                 AND a.opac_visible
7337     $$;
7338     add_peer_query := $$
7339         SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
7340           FROM  asset.copy cp
7341                 JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
7342                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7343                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7344                 JOIN config.copy_status cs ON (cp.status = cs.id)
7345           WHERE NOT cp.deleted
7346                 AND cs.opac_visible
7347                 AND cl.opac_visible
7348                 AND cp.opac_visible
7349                 AND a.opac_visible
7350     $$;
7351     add_front := $$
7352         INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
7353           SELECT id, circ_lib, record FROM (
7354     $$;
7355     add_back := $$
7356         ) AS x
7357     $$;
7358  
7359     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
7360
7361     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
7362         IF TG_OP = 'INSERT' THEN
7363             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.record = ' || NEW.peer_record;
7364             EXECUTE add_front || add_peer_query || add_back;
7365             RETURN NEW;
7366         ELSE
7367             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
7368             EXECUTE remove_query;
7369             RETURN OLD;
7370         END IF;
7371     END IF;
7372
7373     IF TG_OP = 'INSERT' THEN
7374
7375         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7376             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7377             EXECUTE add_front || add_base_query || add_back;
7378         END IF;
7379
7380         RETURN NEW;
7381
7382     END IF;
7383
7384     -- handle items first, since with circulation activity
7385     -- their statuses change frequently
7386     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7387
7388         IF OLD.location    <> NEW.location OR
7389            OLD.call_number <> NEW.call_number OR
7390            OLD.status      <> NEW.status OR
7391            OLD.circ_lib    <> NEW.circ_lib THEN
7392             -- any of these could change visibility, but
7393             -- we'll save some queries and not try to calculate
7394             -- the change directly
7395             do_remove := true;
7396             do_add := true;
7397         ELSE
7398
7399             IF OLD.deleted <> NEW.deleted THEN
7400                 IF NEW.deleted THEN
7401                     do_remove := true;
7402                 ELSE
7403                     do_add := true;
7404                 END IF;
7405             END IF;
7406
7407             IF OLD.opac_visible <> NEW.opac_visible THEN
7408                 IF OLD.opac_visible THEN
7409                     do_remove := true;
7410                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
7411                                         -- is also marked opac_visible
7412                     do_add := true;
7413                 END IF;
7414             END IF;
7415
7416         END IF;
7417
7418         IF do_remove THEN
7419             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
7420         END IF;
7421         IF do_add THEN
7422             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7423             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
7424             EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7425         END IF;
7426
7427         RETURN NEW;
7428
7429     END IF;
7430
7431     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
7432  
7433         IF OLD.deleted AND NEW.deleted THEN -- do nothing
7434
7435             RETURN NEW;
7436  
7437         ELSIF NEW.deleted THEN -- remove rows
7438  
7439             IF TG_TABLE_NAME = 'call_number' THEN
7440                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
7441             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7442                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
7443             END IF;
7444  
7445             RETURN NEW;
7446  
7447         ELSIF OLD.deleted THEN -- add rows
7448  
7449             IF TG_TABLE_NAME = 'call_number' THEN
7450                 add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7451                 EXECUTE add_front || add_base_query || add_back;
7452             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7453                 add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
7454                 add_peer_query := add_peer_query || ' AND pbcm.record = ' || NEW.id;
7455                 EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7456             END IF;
7457  
7458             RETURN NEW;
7459  
7460         END IF;
7461  
7462     END IF;
7463
7464     IF TG_TABLE_NAME = 'call_number' THEN
7465
7466         IF OLD.record <> NEW.record THEN
7467             -- call number is linked to different bib
7468             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
7469             EXECUTE remove_query;
7470             add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7471             EXECUTE add_front || add_base_query || add_back;
7472         END IF;
7473
7474         RETURN NEW;
7475
7476     END IF;
7477
7478     IF TG_TABLE_NAME IN ('record_entry') THEN
7479         RETURN NEW; -- don't have 'opac_visible'
7480     END IF;
7481
7482     -- actor.org_unit, asset.copy_location, asset.copy_status
7483     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
7484
7485         RETURN NEW;
7486
7487     ELSIF NEW.opac_visible THEN -- add rows
7488
7489         IF TG_TABLE_NAME = 'org_unit' THEN
7490             add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id || ';';
7491             add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id || ';';
7492         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7493             add_base_query := add_base_query || ' AND cp.location = ' || NEW.id || ';';
7494             add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id || ';';
7495         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7496             add_base_query := add_base_query || ' AND cp.status = ' || NEW.id || ';';
7497             add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id || ';';
7498         END IF;
7499  
7500         EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7501  
7502     ELSE -- delete rows
7503
7504         IF TG_TABLE_NAME = 'org_unit' THEN
7505             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
7506         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7507             remove_query := remove_query || 'location = ' || NEW.id || ');';
7508         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7509             remove_query := remove_query || 'status = ' || NEW.id || ');';
7510         END IF;
7511  
7512         EXECUTE remove_query;
7513  
7514     END IF;
7515  
7516     RETURN NEW;
7517 END;
7518 $func$ LANGUAGE PLPGSQL;
7519
7520 INSERT INTO config.upgrade_log (version) VALUES ('0569'); --miker
7521
7522 CREATE OR REPLACE FUNCTION unapi.auri ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7523         SELECT  XMLELEMENT(
7524                     name uri,
7525                     XMLATTRIBUTES(
7526                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7527                         'tag:open-ils.org:U2@auri/' || uri.id AS id,
7528                         use_restriction,
7529                         href,
7530                         label
7531                     ),
7532                     XMLELEMENT( name copies,
7533                         CASE
7534                             WHEN ('acn' = ANY ($4)) THEN
7535                                 (SELECT XMLAGG(acn) FROM (SELECT unapi.acn( call_number, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'auri'), $5, $6, $7, $8, FALSE) FROM asset.uri_call_number_map WHERE uri = uri.id)x)
7536                             ELSE NULL
7537                         END
7538                     )
7539                 ) AS x
7540           FROM  asset.uri uri
7541           WHERE uri.id = $1
7542           GROUP BY uri.id, use_restriction, href, label;
7543 $F$ LANGUAGE SQL;
7544
7545 INSERT INTO config.upgrade_log (version) VALUES ('0570');
7546
7547 -- Not everything in 1XX tags should become part of the authorsort field
7548 -- ($0 for example).  The list of subfields chosen here is a superset of all
7549 -- the fields found in the LoC authority mappin definitions for 1XX fields.
7550 -- Anyway, if more fields should be here, add them.
7551
7552 UPDATE config.record_attr_definition
7553     SET sf_list = 'abcdefgklmnopqrstvxyz'
7554     WHERE name='authorsort' AND sf_list IS NULL;
7555
7556 INSERT INTO config.upgrade_log (version) VALUES ('0571');
7557
7558 -- FIXME: add/check SQL statements to perform the upgrade
7559 CREATE OR REPLACE FUNCTION metabib.facet_normalize_trigger () RETURNS TRIGGER AS $$
7560 DECLARE
7561     normalizer  RECORD;
7562     facet_text  TEXT;
7563 BEGIN
7564     facet_text := NEW.value;
7565
7566     FOR normalizer IN
7567         SELECT  n.func AS func,
7568                 n.param_count AS param_count,
7569                 m.params AS params
7570           FROM  config.index_normalizer n
7571                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
7572           WHERE m.field = NEW.field AND m.pos < 0
7573           ORDER BY m.pos LOOP
7574
7575             EXECUTE 'SELECT ' || normalizer.func || '(' ||
7576                 quote_literal( facet_text ) ||
7577                 CASE
7578                     WHEN normalizer.param_count > 0
7579                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
7580                         ELSE ''
7581                     END ||
7582                 ')' INTO facet_text;
7583
7584     END LOOP;
7585
7586     NEW.value = facet_text;
7587
7588     RETURN NEW;
7589 END;
7590 $$ LANGUAGE PLPGSQL;
7591
7592 CREATE TRIGGER facet_normalize_tgr
7593     BEFORE UPDATE OR INSERT ON metabib.facet_entry
7594     FOR EACH ROW EXECUTE PROCEDURE metabib.facet_normalize_trigger();
7595
7596
7597
7598 INSERT INTO config.upgrade_log (version) VALUES ('0578'); -- tsbere via miker
7599
7600 CREATE OR REPLACE VIEW reporter.hold_request_record AS
7601 SELECT  id,
7602         target,
7603         hold_type,
7604         CASE
7605                 WHEN hold_type = 'T'
7606                         THEN target
7607                 WHEN hold_type = 'I'
7608                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
7609                 WHEN hold_type = 'V'
7610                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
7611                 WHEN hold_type IN ('C','R','F')
7612                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
7613                 WHEN hold_type = 'M'
7614                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
7615         WHEN hold_type = 'P'
7616             THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
7617         END AS bib_record
7618   FROM  action.hold_request ahr;
7619
7620 INSERT INTO config.upgrade_log (version) VALUES ('0583');
7621
7622 CREATE OR REPLACE VIEW action.all_circulation AS
7623     SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
7624         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
7625         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
7626         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
7627         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
7628         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
7629       FROM  action.aged_circulation
7630             UNION ALL
7631     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,
7632         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,
7633         cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
7634         circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
7635         circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
7636         circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
7637         circ.parent_circ
7638       FROM  action.circulation circ
7639         JOIN asset.copy cp ON (circ.target_copy = cp.id)
7640         JOIN asset.call_number cn ON (cp.call_number = cn.id)
7641         JOIN actor.usr p ON (circ.usr = p.id)
7642         LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
7643         LEFT JOIN actor.usr_address b ON (p.billing_address = b.id);
7644
7645
7646
7647 INSERT INTO config.upgrade_log (version) VALUES ('0590'); -- miker/tsbere
7648
7649 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
7650 DECLARE
7651     add_front       TEXT;
7652     add_back        TEXT;
7653     add_base_query  TEXT;
7654     add_peer_query  TEXT;
7655     remove_query    TEXT;
7656     do_add          BOOLEAN := false;
7657     do_remove       BOOLEAN := false;
7658 BEGIN
7659     add_base_query := $$
7660         SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
7661           FROM  asset.copy cp
7662                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
7663                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7664                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7665                 JOIN config.copy_status cs ON (cp.status = cs.id)
7666                 JOIN biblio.record_entry b ON (cn.record = b.id)
7667           WHERE NOT cp.deleted
7668                 AND NOT cn.deleted
7669                 AND NOT b.deleted
7670                 AND cs.opac_visible
7671                 AND cl.opac_visible
7672                 AND cp.opac_visible
7673                 AND a.opac_visible
7674     $$;
7675     add_peer_query := $$
7676         SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
7677           FROM  asset.copy cp
7678                 JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
7679                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7680                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7681                 JOIN config.copy_status cs ON (cp.status = cs.id)
7682           WHERE NOT cp.deleted
7683                 AND cs.opac_visible
7684                 AND cl.opac_visible
7685                 AND cp.opac_visible
7686                 AND a.opac_visible
7687     $$;
7688     add_front := $$
7689         INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
7690           SELECT id, circ_lib, record FROM (
7691     $$;
7692     add_back := $$
7693         ) AS x
7694     $$;
7695  
7696     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
7697
7698     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
7699         IF TG_OP = 'INSERT' THEN
7700             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.record = ' || NEW.peer_record;
7701             EXECUTE add_front || add_peer_query || add_back;
7702             RETURN NEW;
7703         ELSE
7704             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
7705             EXECUTE remove_query;
7706             RETURN OLD;
7707         END IF;
7708     END IF;
7709
7710     IF TG_OP = 'INSERT' THEN
7711
7712         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7713             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7714             EXECUTE add_front || add_base_query || add_back;
7715         END IF;
7716
7717         RETURN NEW;
7718
7719     END IF;
7720
7721     -- handle items first, since with circulation activity
7722     -- their statuses change frequently
7723     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7724
7725         IF OLD.location    <> NEW.location OR
7726            OLD.call_number <> NEW.call_number OR
7727            OLD.status      <> NEW.status OR
7728            OLD.circ_lib    <> NEW.circ_lib THEN
7729             -- any of these could change visibility, but
7730             -- we'll save some queries and not try to calculate
7731             -- the change directly
7732             do_remove := true;
7733             do_add := true;
7734         ELSE
7735
7736             IF OLD.deleted <> NEW.deleted THEN
7737                 IF NEW.deleted THEN
7738                     do_remove := true;
7739                 ELSE
7740                     do_add := true;
7741                 END IF;
7742             END IF;
7743
7744             IF OLD.opac_visible <> NEW.opac_visible THEN
7745                 IF OLD.opac_visible THEN
7746                     do_remove := true;
7747                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
7748                                         -- is also marked opac_visible
7749                     do_add := true;
7750                 END IF;
7751             END IF;
7752
7753         END IF;
7754
7755         IF do_remove THEN
7756             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
7757         END IF;
7758         IF do_add THEN
7759             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7760             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
7761             EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7762         END IF;
7763
7764         RETURN NEW;
7765
7766     END IF;
7767
7768     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
7769  
7770         IF OLD.deleted AND NEW.deleted THEN -- do nothing
7771
7772             RETURN NEW;
7773  
7774         ELSIF NEW.deleted THEN -- remove rows
7775  
7776             IF TG_TABLE_NAME = 'call_number' THEN
7777                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
7778             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7779                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
7780             END IF;
7781  
7782             RETURN NEW;
7783  
7784         ELSIF OLD.deleted THEN -- add rows
7785  
7786             IF TG_TABLE_NAME = 'call_number' THEN
7787                 add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7788                 EXECUTE add_front || add_base_query || add_back;
7789             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7790                 add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
7791                 add_peer_query := add_peer_query || ' AND pbcm.record = ' || NEW.id;
7792                 EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7793             END IF;
7794  
7795             RETURN NEW;
7796  
7797         END IF;
7798  
7799     END IF;
7800
7801     IF TG_TABLE_NAME = 'call_number' THEN
7802
7803         IF OLD.record <> NEW.record THEN
7804             -- call number is linked to different bib
7805             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
7806             EXECUTE remove_query;
7807             add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7808             EXECUTE add_front || add_base_query || add_back;
7809         END IF;
7810
7811         RETURN NEW;
7812
7813     END IF;
7814
7815     IF TG_TABLE_NAME IN ('record_entry') THEN
7816         RETURN NEW; -- don't have 'opac_visible'
7817     END IF;
7818
7819     -- actor.org_unit, asset.copy_location, asset.copy_status
7820     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
7821
7822         RETURN NEW;
7823
7824     ELSIF NEW.opac_visible THEN -- add rows
7825
7826         IF TG_TABLE_NAME = 'org_unit' THEN
7827             add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id;
7828             add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id;
7829         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7830             add_base_query := add_base_query || ' AND cp.location = ' || NEW.id;
7831             add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id;
7832         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7833             add_base_query := add_base_query || ' AND cp.status = ' || NEW.id;
7834             add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id;
7835         END IF;
7836  
7837         EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7838  
7839     ELSE -- delete rows
7840
7841         IF TG_TABLE_NAME = 'org_unit' THEN
7842             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
7843         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7844             remove_query := remove_query || 'location = ' || NEW.id || ');';
7845         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7846             remove_query := remove_query || 'status = ' || NEW.id || ');';
7847         END IF;
7848  
7849         EXECUTE remove_query;
7850  
7851     END IF;
7852  
7853     RETURN NEW;
7854 END;
7855 $func$ LANGUAGE PLPGSQL;
7856
7857 INSERT INTO config.upgrade_log (version) VALUES ('0591'); -- berick/miker
7858
7859 CREATE OR REPLACE FUNCTION action.usr_visible_circs (usr_id INT) RETURNS SETOF action.circulation AS $func$
7860 DECLARE
7861     c               action.circulation%ROWTYPE;
7862     view_age        INTERVAL;
7863     usr_view_age    actor.usr_setting%ROWTYPE;
7864     usr_view_start  actor.usr_setting%ROWTYPE;
7865 BEGIN
7866     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_age';
7867     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_start';
7868
7869     IF usr_view_age.value IS NOT NULL AND usr_view_start.value IS NOT NULL THEN
7870         -- User opted in and supplied a retention age
7871         IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
7872             view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7873         ELSE
7874             view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
7875         END IF;
7876     ELSIF usr_view_start.value IS NOT NULL THEN
7877         -- User opted in
7878         view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7879     ELSE
7880         -- User did not opt in
7881         RETURN;
7882     END IF;
7883
7884     FOR c IN
7885         SELECT  *
7886           FROM  action.circulation
7887           WHERE usr = usr_id
7888                 AND parent_circ IS NULL
7889                 AND xact_start > NOW() - view_age
7890           ORDER BY xact_start DESC
7891     LOOP
7892         RETURN NEXT c;
7893     END LOOP;
7894
7895     RETURN;
7896 END;
7897 $func$ LANGUAGE PLPGSQL;
7898
7899 CREATE OR REPLACE FUNCTION action.usr_visible_holds (usr_id INT) RETURNS SETOF action.hold_request AS $func$
7900 DECLARE
7901     h               action.hold_request%ROWTYPE;
7902     view_age        INTERVAL;
7903     view_count      INT;
7904     usr_view_count  actor.usr_setting%ROWTYPE;
7905     usr_view_age    actor.usr_setting%ROWTYPE;
7906     usr_view_start  actor.usr_setting%ROWTYPE;
7907 BEGIN
7908     SELECT * INTO usr_view_count FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_count';
7909     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_age';
7910     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_start';
7911
7912     FOR h IN
7913         SELECT  *
7914           FROM  action.hold_request
7915           WHERE usr = usr_id
7916                 AND fulfillment_time IS NULL
7917                 AND cancel_time IS NULL
7918           ORDER BY request_time DESC
7919     LOOP
7920         RETURN NEXT h;
7921     END LOOP;
7922
7923     IF usr_view_start.value IS NULL THEN
7924         RETURN;
7925     END IF;
7926
7927     IF usr_view_age.value IS NOT NULL THEN
7928         -- User opted in and supplied a retention age
7929         IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
7930             view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7931         ELSE
7932             view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
7933         END IF;
7934     ELSE
7935         -- User opted in
7936         view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7937     END IF;
7938
7939     IF usr_view_count.value IS NOT NULL THEN
7940         view_count := oils_json_to_text(usr_view_count.value)::INT;
7941     ELSE
7942         view_count := 1000;
7943     END IF;
7944
7945     -- show some fulfilled/canceled holds
7946     FOR h IN
7947         SELECT  *
7948           FROM  action.hold_request
7949           WHERE usr = usr_id
7950                 AND ( fulfillment_time IS NOT NULL OR cancel_time IS NOT NULL )
7951                 AND request_time > NOW() - view_age
7952           ORDER BY request_time DESC
7953           LIMIT view_count
7954     LOOP
7955         RETURN NEXT h;
7956     END LOOP;
7957
7958     RETURN;
7959 END;
7960 $func$ LANGUAGE PLPGSQL;
7961
7962 INSERT INTO config.upgrade_log (version) VALUES ('0599'); -- miker/gmc
7963
7964 UPDATE config.metabib_field 
7965 SET xpath = $$//mods32:mods/mods32:name[@type='personal' and not(mods32:role/mods32:roleTerm[text()='creator'])]$$
7966 WHERE field_class = 'author'
7967 AND name = 'other'
7968 AND xpath = $$//mods32:mods/mods32:name[@type='personal' and not(mods32:role)]$$
7969 AND format = 'mods32';
7970
7971 \qecho To reindex bibs that use the author|other index definition,
7972 \qecho you can run something like this:
7973 \qecho
7974 \qecho SELECT metabib.reingest_metabib_field_entries(record)
7975 \qecho FROM (
7976 \qecho   SELECT DISTINCT record
7977 \qecho   FROM metabib.real_full_rec
7978 \qecho   WHERE tag IN ('600', '700', '720', '800')
7979 \qecho   AND   subfield IN ('4', 'e')
7980 \qecho ) a;
7981
7982 -- Resolves an error in calculating copy counts for org lassos
7983 -- Per LP 790329
7984 INSERT INTO config.upgrade_log (version) VALUES ('0603');
7985
7986 -- FIXME: add/check SQL statements to perform the upgrade
7987 CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
7988 DECLARE
7989     ans RECORD;
7990     trans INT;
7991 BEGIN
7992     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
7993
7994     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
7995         RETURN QUERY
7996         SELECT  -1,
7997                 ans.id,
7998                 COUNT( av.id ),
7999                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
8000                 COUNT( av.id ),
8001                 trans
8002           FROM
8003                 actor.org_unit_descendants(ans.id) d
8004                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
8005                 JOIN asset.copy cp ON (cp.id = av.copy_id)
8006           GROUP BY 1,2,6;
8007
8008         IF NOT FOUND THEN
8009             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
8010         END IF;
8011
8012     END LOOP;   
8013                 
8014     RETURN;     
8015 END;            
8016 $f$ LANGUAGE PLPGSQL;
8017
8018
8019 -- Staff record copy counts also triggered an SQL error for org lassos
8020 -- Per LP790329
8021 --
8022 INSERT INTO config.upgrade_log (version) VALUES ('0604');
8023
8024 CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
8025 DECLARE
8026     ans RECORD;
8027     trans INT;
8028 BEGIN
8029     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
8030
8031     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
8032         RETURN QUERY
8033         SELECT  -1,
8034                 ans.id,
8035                 COUNT( cp.id ),
8036                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
8037                 COUNT( cp.id ),
8038                 trans
8039           FROM
8040                 actor.org_unit_descendants(ans.id) d
8041                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
8042                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
8043           GROUP BY 1,2,6;
8044
8045         IF NOT FOUND THEN
8046             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
8047         END IF;
8048
8049     END LOOP;
8050
8051     RETURN;
8052 END;
8053 $f$ LANGUAGE PLPGSQL;
8054
8055 INSERT INTO config.upgrade_log (version) VALUES ('0614'); --miker/phasefx
8056
8057 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
8058 DECLARE
8059     add_front       TEXT;
8060     add_back        TEXT;
8061     add_base_query  TEXT;
8062     add_peer_query  TEXT;
8063     remove_query    TEXT;
8064     do_add          BOOLEAN := false;
8065     do_remove       BOOLEAN := false;
8066 BEGIN
8067     add_base_query := $$
8068         SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
8069           FROM  asset.copy cp
8070                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
8071                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
8072                 JOIN asset.copy_location cl ON (cp.location = cl.id)
8073                 JOIN config.copy_status cs ON (cp.status = cs.id)
8074                 JOIN biblio.record_entry b ON (cn.record = b.id)
8075           WHERE NOT cp.deleted
8076                 AND NOT cn.deleted
8077                 AND NOT b.deleted
8078                 AND cs.opac_visible
8079                 AND cl.opac_visible
8080                 AND cp.opac_visible
8081                 AND a.opac_visible
8082     $$;
8083     add_peer_query := $$
8084         SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
8085           FROM  asset.copy cp
8086                 JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
8087                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
8088                 JOIN asset.copy_location cl ON (cp.location = cl.id)
8089                 JOIN config.copy_status cs ON (cp.status = cs.id)
8090           WHERE NOT cp.deleted
8091                 AND cs.opac_visible
8092                 AND cl.opac_visible
8093                 AND cp.opac_visible
8094                 AND a.opac_visible
8095     $$;
8096     add_front := $$
8097         INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
8098           SELECT id, circ_lib, record FROM (
8099     $$;
8100     add_back := $$
8101         ) AS x
8102     $$;
8103  
8104     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
8105
8106     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
8107         IF TG_OP = 'INSERT' THEN
8108             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.peer_record = ' || NEW.peer_record;
8109             EXECUTE add_front || add_peer_query || add_back;
8110             RETURN NEW;
8111         ELSE
8112             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
8113             EXECUTE remove_query;
8114             RETURN OLD;
8115         END IF;
8116     END IF;
8117
8118     IF TG_OP = 'INSERT' THEN
8119
8120         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
8121             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
8122             EXECUTE add_front || add_base_query || add_back;
8123         END IF;
8124
8125         RETURN NEW;
8126
8127     END IF;
8128
8129     -- handle items first, since with circulation activity
8130     -- their statuses change frequently
8131     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
8132
8133         IF OLD.location    <> NEW.location OR
8134            OLD.call_number <> NEW.call_number OR
8135            OLD.status      <> NEW.status OR
8136            OLD.circ_lib    <> NEW.circ_lib THEN
8137             -- any of these could change visibility, but
8138             -- we'll save some queries and not try to calculate
8139             -- the change directly
8140             do_remove := true;
8141             do_add := true;
8142         ELSE
8143
8144             IF OLD.deleted <> NEW.deleted THEN
8145                 IF NEW.deleted THEN
8146                     do_remove := true;
8147                 ELSE
8148                     do_add := true;
8149                 END IF;
8150             END IF;
8151
8152             IF OLD.opac_visible <> NEW.opac_visible THEN
8153                 IF OLD.opac_visible THEN
8154                     do_remove := true;
8155                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
8156                                         -- is also marked opac_visible
8157                     do_add := true;
8158                 END IF;
8159             END IF;
8160
8161         END IF;
8162
8163         IF do_remove THEN
8164             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
8165         END IF;
8166         IF do_add THEN
8167             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
8168             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
8169             EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
8170         END IF;
8171
8172         RETURN NEW;
8173
8174     END IF;
8175
8176     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
8177  
8178         IF OLD.deleted AND NEW.deleted THEN -- do nothing
8179
8180             RETURN NEW;
8181  
8182         ELSIF NEW.deleted THEN -- remove rows
8183  
8184             IF TG_TABLE_NAME = 'call_number' THEN
8185                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
8186             ELSIF TG_TABLE_NAME = 'record_entry' THEN
8187                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
8188             END IF;
8189  
8190             RETURN NEW;
8191  
8192         ELSIF OLD.deleted THEN -- add rows
8193  
8194             IF TG_TABLE_NAME = 'call_number' THEN
8195                 add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
8196                 EXECUTE add_front || add_base_query || add_back;
8197             ELSIF TG_TABLE_NAME = 'record_entry' THEN
8198                 add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
8199                 add_peer_query := add_peer_query || ' AND pbcm.peer_record = ' || NEW.id;
8200                 EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
8201             END IF;
8202  
8203             RETURN NEW;
8204  
8205         END IF;
8206  
8207     END IF;
8208
8209     IF TG_TABLE_NAME = 'call_number' THEN
8210
8211         IF OLD.record <> NEW.record THEN
8212             -- call number is linked to different bib
8213             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
8214             EXECUTE remove_query;
8215             add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
8216             EXECUTE add_front || add_base_query || add_back;
8217         END IF;
8218
8219         RETURN NEW;
8220
8221     END IF;
8222
8223     IF TG_TABLE_NAME IN ('record_entry') THEN
8224         RETURN NEW; -- don't have 'opac_visible'
8225     END IF;
8226
8227     -- actor.org_unit, asset.copy_location, asset.copy_status
8228     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
8229
8230         RETURN NEW;
8231
8232     ELSIF NEW.opac_visible THEN -- add rows
8233
8234         IF TG_TABLE_NAME = 'org_unit' THEN
8235             add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id;
8236             add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id;
8237         ELSIF TG_TABLE_NAME = 'copy_location' THEN
8238             add_base_query := add_base_query || ' AND cp.location = ' || NEW.id;
8239             add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id;
8240         ELSIF TG_TABLE_NAME = 'copy_status' THEN
8241             add_base_query := add_base_query || ' AND cp.status = ' || NEW.id;
8242             add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id;
8243         END IF;
8244  
8245         EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
8246  
8247     ELSE -- delete rows
8248
8249         IF TG_TABLE_NAME = 'org_unit' THEN
8250             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
8251         ELSIF TG_TABLE_NAME = 'copy_location' THEN
8252             remove_query := remove_query || 'location = ' || NEW.id || ');';
8253         ELSIF TG_TABLE_NAME = 'copy_status' THEN
8254             remove_query := remove_query || 'status = ' || NEW.id || ');';
8255         END IF;
8256  
8257         EXECUTE remove_query;
8258  
8259     END IF;
8260  
8261     RETURN NEW;
8262 END;
8263 $func$ LANGUAGE PLPGSQL;
8264
8265 INSERT INTO config.upgrade_log (version) VALUES ('0579'); -- superceded by 0620
8266 INSERT INTO config.upgrade_log (version) VALUES ('0620'); -- tsbere via miker
8267
8268 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
8269 DECLARE
8270     user_object             actor.usr%ROWTYPE;
8271     standing_penalty        config.standing_penalty%ROWTYPE;
8272     item_object             asset.copy%ROWTYPE;
8273     item_status_object      config.copy_status%ROWTYPE;
8274     item_location_object    asset.copy_location%ROWTYPE;
8275     result                  action.circ_matrix_test_result;
8276     circ_test               action.found_circ_matrix_matchpoint;
8277     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
8278     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
8279     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
8280     hold_ratio              action.hold_stats%ROWTYPE;
8281     penalty_type            TEXT;
8282     items_out               INT;
8283     context_org_list        INT[];
8284     done                    BOOL := FALSE;
8285 BEGIN
8286     -- Assume success unless we hit a failure condition
8287     result.success := TRUE;
8288
8289     -- Need user info to look up matchpoints
8290     SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
8291
8292     -- (Insta)Fail if we couldn't find the user
8293     IF user_object.id IS NULL THEN
8294         result.fail_part := 'no_user';
8295         result.success := FALSE;
8296         done := TRUE;
8297         RETURN NEXT result;
8298         RETURN;
8299     END IF;
8300
8301     -- Need item info to look up matchpoints
8302     SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
8303
8304     -- (Insta)Fail if we couldn't find the item 
8305     IF item_object.id IS NULL THEN
8306         result.fail_part := 'no_item';
8307         result.success := FALSE;
8308         done := TRUE;
8309         RETURN NEXT result;
8310         RETURN;
8311     END IF;
8312
8313     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
8314
8315     circ_matchpoint             := circ_test.matchpoint;
8316     result.matchpoint           := circ_matchpoint.id;
8317     result.circulate            := circ_matchpoint.circulate;
8318     result.duration_rule        := circ_matchpoint.duration_rule;
8319     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
8320     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
8321     result.hard_due_date        := circ_matchpoint.hard_due_date;
8322     result.renewals             := circ_matchpoint.renewals;
8323     result.grace_period         := circ_matchpoint.grace_period;
8324     result.buildrows            := circ_test.buildrows;
8325
8326     -- (Insta)Fail if we couldn't find a matchpoint
8327     IF circ_test.success = false THEN
8328         result.fail_part := 'no_matchpoint';
8329         result.success := FALSE;
8330         done := TRUE;
8331         RETURN NEXT result;
8332         RETURN;
8333     END IF;
8334
8335     -- All failures before this point are non-recoverable
8336     -- Below this point are possibly overridable failures
8337
8338     -- Fail if the user is barred
8339     IF user_object.barred IS TRUE THEN
8340         result.fail_part := 'actor.usr.barred';
8341         result.success := FALSE;
8342         done := TRUE;
8343         RETURN NEXT result;
8344     END IF;
8345
8346     -- Fail if the item can't circulate
8347     IF item_object.circulate IS FALSE THEN
8348         result.fail_part := 'asset.copy.circulate';
8349         result.success := FALSE;
8350         done := TRUE;
8351         RETURN NEXT result;
8352     END IF;
8353
8354     -- Fail if the item isn't in a circulateable status on a non-renewal
8355     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
8356         result.fail_part := 'asset.copy.status';
8357         result.success := FALSE;
8358         done := TRUE;
8359         RETURN NEXT result;
8360     -- Alternately, fail if the item isn't checked out on a renewal
8361     ELSIF renewal AND item_object.status <> 1 THEN
8362         result.fail_part := 'asset.copy.status';
8363         result.success := FALSE;
8364         done := TRUE;
8365         RETURN NEXT result;
8366     END IF;
8367
8368     -- Fail if the item can't circulate because of the shelving location
8369     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
8370     IF item_location_object.circulate IS FALSE THEN
8371         result.fail_part := 'asset.copy_location.circulate';
8372         result.success := FALSE;
8373         done := TRUE;
8374         RETURN NEXT result;
8375     END IF;
8376
8377     -- Use Circ OU for penalties and such
8378     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_ou );
8379
8380     IF renewal THEN
8381         penalty_type = '%RENEW%';
8382     ELSE
8383         penalty_type = '%CIRC%';
8384     END IF;
8385
8386     FOR standing_penalty IN
8387         SELECT  DISTINCT csp.*
8388           FROM  actor.usr_standing_penalty usp
8389                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
8390           WHERE usr = match_user
8391                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
8392                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
8393                 AND csp.block_list LIKE penalty_type LOOP
8394
8395         result.fail_part := standing_penalty.name;
8396         result.success := FALSE;
8397         done := TRUE;
8398         RETURN NEXT result;
8399     END LOOP;
8400
8401     -- Fail if the test is set to hard non-circulating
8402     IF circ_matchpoint.circulate IS FALSE THEN
8403         result.fail_part := 'config.circ_matrix_test.circulate';
8404         result.success := FALSE;
8405         done := TRUE;
8406         RETURN NEXT result;
8407     END IF;
8408
8409     -- Fail if the total copy-hold ratio is too low
8410     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
8411         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
8412         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
8413             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
8414             result.success := FALSE;
8415             done := TRUE;
8416             RETURN NEXT result;
8417         END IF;
8418     END IF;
8419
8420     -- Fail if the available copy-hold ratio is too low
8421     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
8422         IF hold_ratio.hold_count IS NULL THEN
8423             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
8424         END IF;
8425         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
8426             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
8427             result.success := FALSE;
8428             done := TRUE;
8429             RETURN NEXT result;
8430         END IF;
8431     END IF;
8432
8433     -- Fail if the user has too many items with specific circ_modifiers checked out
8434     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
8435         SELECT  INTO items_out COUNT(*)
8436           FROM  action.circulation circ
8437             JOIN asset.copy cp ON (cp.id = circ.target_copy)
8438           WHERE circ.usr = match_user
8439                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
8440             AND circ.checkin_time IS NULL
8441             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
8442             AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
8443         IF items_out >= out_by_circ_mod.items_out THEN
8444             result.fail_part := 'config.circ_matrix_circ_mod_test';
8445             result.success := FALSE;
8446             done := TRUE;
8447             RETURN NEXT result;
8448         END IF;
8449     END LOOP;
8450
8451     -- If we passed everything, return the successful matchpoint
8452     IF NOT done THEN
8453         RETURN NEXT result;
8454     END IF;
8455
8456     RETURN;
8457 END;
8458 $func$ LANGUAGE plpgsql;
8459
8460
8461
8462 INSERT INTO config.upgrade_log (version) VALUES ('0628');
8463
8464 -- acq.fund_combined_balance and acq.fund_spent_balance are unchanged,
8465 -- however we need to drop them to recreate the other views.
8466 -- we need to drop all our views because we change the number of columns
8467 -- for example, debit_total does not need an encumberance column when we 
8468 -- have a sepearate total for that.
8469
8470 DROP VIEW acq.fund_spent_balance;
8471 DROP VIEW acq.fund_combined_balance;
8472 DROP VIEW acq.fund_encumbrance_total;
8473 DROP VIEW acq.fund_spent_total;
8474 DROP VIEW acq.fund_debit_total;
8475
8476 CREATE OR REPLACE VIEW acq.fund_debit_total AS
8477     SELECT  fund.id AS fund, 
8478             sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount
8479     FROM acq.fund fund
8480         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund
8481     GROUP BY fund.id;
8482
8483 CREATE OR REPLACE VIEW acq.fund_encumbrance_total AS
8484     SELECT 
8485         fund.id AS fund, 
8486         sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount 
8487     FROM acq.fund fund
8488         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund 
8489     WHERE fund_debit.encumbrance GROUP BY fund.id;
8490
8491 CREATE OR REPLACE VIEW acq.fund_spent_total AS
8492     SELECT  fund.id AS fund, 
8493             sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount 
8494     FROM acq.fund fund
8495         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund 
8496     WHERE NOT fund_debit.encumbrance 
8497     GROUP BY fund.id;
8498
8499 CREATE OR REPLACE VIEW acq.fund_combined_balance AS
8500     SELECT  c.fund, 
8501             c.amount - COALESCE(d.amount, 0.0) AS amount
8502     FROM acq.fund_allocation_total c
8503     LEFT JOIN acq.fund_debit_total d USING (fund);
8504
8505 CREATE OR REPLACE VIEW acq.fund_spent_balance AS
8506     SELECT  c.fund,
8507             c.amount - COALESCE(d.amount,0.0) AS amount
8508       FROM  acq.fund_allocation_total c
8509             LEFT JOIN acq.fund_spent_total d USING (fund);
8510
8511
8512
8513 INSERT INTO config.upgrade_log (version) VALUES ('0631');
8514
8515 CREATE OR REPLACE FUNCTION search.query_parser_fts (
8516
8517     param_search_ou INT,
8518     param_depth     INT,
8519     param_query     TEXT,
8520     param_statuses  INT[],
8521     param_locations INT[],
8522     param_offset    INT,
8523     param_check     INT,
8524     param_limit     INT,
8525     metarecord      BOOL,
8526     staff           BOOL
8527  
8528 ) RETURNS SETOF search.search_result AS $func$
8529 DECLARE
8530
8531     current_res         search.search_result%ROWTYPE;
8532     search_org_list     INT[];
8533     luri_org_list       INT[];
8534     tmp_int_list        INT[];
8535
8536     check_limit         INT;
8537     core_limit          INT;
8538     core_offset         INT;
8539     tmp_int             INT;
8540
8541     core_result         RECORD;
8542     core_cursor         REFCURSOR;
8543     core_rel_query      TEXT;
8544
8545     total_count         INT := 0;
8546     check_count         INT := 0;
8547     deleted_count       INT := 0;
8548     visible_count       INT := 0;
8549     excluded_count      INT := 0;
8550
8551 BEGIN
8552
8553     check_limit := COALESCE( param_check, 1000 );
8554     core_limit  := COALESCE( param_limit, 25000 );
8555     core_offset := COALESCE( param_offset, 0 );
8556
8557     -- core_skip_chk := COALESCE( param_skip_chk, 1 );
8558
8559     IF param_search_ou > 0 THEN
8560         IF param_depth IS NOT NULL THEN
8561             SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
8562         ELSE
8563             SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
8564         END IF;
8565
8566         SELECT array_accum(distinct id) INTO luri_org_list FROM actor.org_unit_ancestors( param_search_ou );
8567
8568     ELSIF param_search_ou < 0 THEN
8569         SELECT array_accum(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
8570
8571         FOR tmp_int IN SELECT * FROM UNNEST(search_org_list) LOOP
8572             SELECT array_accum(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( tmp_int );
8573             luri_org_list := luri_org_list || tmp_int_list;
8574         END LOOP;
8575
8576         SELECT array_accum(DISTINCT x.id) INTO luri_org_list FROM UNNEST(luri_org_list) x(id);
8577
8578     ELSIF param_search_ou = 0 THEN
8579         -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
8580     END IF;
8581
8582     OPEN core_cursor FOR EXECUTE param_query;
8583
8584     LOOP
8585
8586         FETCH core_cursor INTO core_result;
8587         EXIT WHEN NOT FOUND;
8588         EXIT WHEN total_count >= core_limit;
8589
8590         total_count := total_count + 1;
8591
8592         CONTINUE WHEN total_count NOT BETWEEN  core_offset + 1 AND check_limit + core_offset;
8593
8594         check_count := check_count + 1;
8595
8596         PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
8597         IF NOT FOUND THEN
8598             -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
8599             deleted_count := deleted_count + 1;
8600             CONTINUE;
8601         END IF;
8602
8603         PERFORM 1
8604           FROM  biblio.record_entry b
8605                 JOIN config.bib_source s ON (b.source = s.id)
8606           WHERE s.transcendant
8607                 AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
8608
8609         IF FOUND THEN
8610             -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
8611             visible_count := visible_count + 1;
8612
8613             current_res.id = core_result.id;
8614             current_res.rel = core_result.rel;
8615
8616             tmp_int := 1;
8617             IF metarecord THEN
8618                 SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
8619             END IF;
8620
8621             IF tmp_int = 1 THEN
8622                 current_res.record = core_result.records[1];
8623             ELSE
8624                 current_res.record = NULL;
8625             END IF;
8626
8627             RETURN NEXT current_res;
8628
8629             CONTINUE;
8630         END IF;
8631
8632         PERFORM 1
8633           FROM  asset.call_number cn
8634                 JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
8635                 JOIN asset.uri uri ON (map.uri = uri.id)
8636           WHERE NOT cn.deleted
8637                 AND cn.label = '##URI##'
8638                 AND uri.active
8639                 AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
8640                 AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8641                 AND cn.owning_lib IN ( SELECT * FROM unnest( luri_org_list ) )
8642           LIMIT 1;
8643
8644         IF FOUND THEN
8645             -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
8646             visible_count := visible_count + 1;
8647
8648             current_res.id = core_result.id;
8649             current_res.rel = core_result.rel;
8650
8651             tmp_int := 1;
8652             IF metarecord THEN
8653                 SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
8654             END IF;
8655
8656             IF tmp_int = 1 THEN
8657                 current_res.record = core_result.records[1];
8658             ELSE
8659                 current_res.record = NULL;
8660             END IF;
8661
8662             RETURN NEXT current_res;
8663
8664             CONTINUE;
8665         END IF;
8666
8667         IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
8668
8669             PERFORM 1
8670               FROM  asset.call_number cn
8671                     JOIN asset.copy cp ON (cp.call_number = cn.id)
8672               WHERE NOT cn.deleted
8673                     AND NOT cp.deleted
8674                     AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
8675                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8676                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8677               LIMIT 1;
8678
8679             IF NOT FOUND THEN
8680                 PERFORM 1
8681                   FROM  biblio.peer_bib_copy_map pr
8682                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
8683                   WHERE NOT cp.deleted
8684                         AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
8685                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8686                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8687                   LIMIT 1;
8688
8689                 IF NOT FOUND THEN
8690                 -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
8691                     excluded_count := excluded_count + 1;
8692                     CONTINUE;
8693                 END IF;
8694             END IF;
8695
8696         END IF;
8697
8698         IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
8699
8700             PERFORM 1
8701               FROM  asset.call_number cn
8702                     JOIN asset.copy cp ON (cp.call_number = cn.id)
8703               WHERE NOT cn.deleted
8704                     AND NOT cp.deleted
8705                     AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
8706                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8707                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8708               LIMIT 1;
8709
8710             IF NOT FOUND THEN
8711                 PERFORM 1
8712                   FROM  biblio.peer_bib_copy_map pr
8713                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
8714                   WHERE NOT cp.deleted
8715                         AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
8716                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8717                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8718                   LIMIT 1;
8719
8720                 IF NOT FOUND THEN
8721                     -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
8722                     excluded_count := excluded_count + 1;
8723                     CONTINUE;
8724                 END IF;
8725             END IF;
8726
8727         END IF;
8728
8729         IF staff IS NULL OR NOT staff THEN
8730
8731             PERFORM 1
8732               FROM  asset.opac_visible_copies
8733               WHERE circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8734                     AND record IN ( SELECT * FROM unnest( core_result.records ) )
8735               LIMIT 1;
8736
8737             IF NOT FOUND THEN
8738                 PERFORM 1
8739                   FROM  biblio.peer_bib_copy_map pr
8740                         JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
8741                   WHERE cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8742                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8743                   LIMIT 1;
8744
8745                 IF NOT FOUND THEN
8746
8747                     -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
8748                     excluded_count := excluded_count + 1;
8749                     CONTINUE;
8750                 END IF;
8751             END IF;
8752
8753         ELSE
8754
8755             PERFORM 1
8756               FROM  asset.call_number cn
8757                     JOIN asset.copy cp ON (cp.call_number = cn.id)
8758               WHERE NOT cn.deleted
8759                     AND NOT cp.deleted
8760                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8761                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8762               LIMIT 1;
8763
8764             IF NOT FOUND THEN
8765
8766                 PERFORM 1
8767                   FROM  biblio.peer_bib_copy_map pr
8768                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
8769                   WHERE NOT cp.deleted
8770                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8771                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8772                   LIMIT 1;
8773
8774                 IF NOT FOUND THEN
8775
8776                     PERFORM 1
8777                       FROM  asset.call_number cn
8778                             JOIN asset.copy cp ON (cp.call_number = cn.id)
8779                       WHERE cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8780                             AND NOT cp.deleted
8781                       LIMIT 1;
8782
8783                     IF FOUND THEN
8784                         -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
8785                         excluded_count := excluded_count + 1;
8786                         CONTINUE;
8787                     END IF;
8788                 END IF;
8789
8790             END IF;
8791
8792         END IF;
8793
8794         visible_count := visible_count + 1;
8795
8796         current_res.id = core_result.id;
8797         current_res.rel = core_result.rel;
8798
8799         tmp_int := 1;
8800         IF metarecord THEN
8801             SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
8802         END IF;
8803
8804         IF tmp_int = 1 THEN
8805             current_res.record = core_result.records[1];
8806         ELSE
8807             current_res.record = NULL;
8808         END IF;
8809
8810         RETURN NEXT current_res;
8811
8812         IF visible_count % 1000 = 0 THEN
8813             -- RAISE NOTICE ' % visible so far ... ', visible_count;
8814         END IF;
8815
8816     END LOOP;
8817
8818     current_res.id = NULL;
8819     current_res.rel = NULL;
8820     current_res.record = NULL;
8821     current_res.total = total_count;
8822     current_res.checked = check_count;
8823     current_res.deleted = deleted_count;
8824     current_res.visible = visible_count;
8825     current_res.excluded = excluded_count;
8826
8827     CLOSE core_cursor;
8828
8829     RETURN NEXT current_res;
8830
8831 END;
8832 $func$ LANGUAGE PLPGSQL;
8833
8834
8835 INSERT INTO config.upgrade_log (version) VALUES ('0633');
8836 INSERT INTO config.upgrade_log (version) VALUES ('0634');
8837
8838 COMMIT;
8839
8840 --0633
8841 INSERT into config.org_unit_setting_type
8842 ( name, grp, label, description, datatype ) VALUES
8843 (
8844         'print.custom_js_file', 'circ',
8845         oils_i18n_gettext(
8846             'print.custom_js_file',
8847             'Printing: Custom Javascript File',
8848             'coust',
8849             'label'
8850         ),
8851         oils_i18n_gettext(
8852             'print.custom_js_file',
8853             'Full URL path to a Javascript File to be loaded when printing. Should'
8854             || ' implement a print_custom function for DOM manipulation. Can change'
8855             || ' the value of the do_print variable to false to cancel printing.',
8856             'coust',
8857             'description'
8858         ),
8859         'string'
8860     );
8861
8862
8863 --0634
8864 INSERT INTO permission.perm_list ( id, code, description ) VALUES
8865  ( 513, 'DEBUG_CLIENT', oils_i18n_gettext( 513,
8866     'Allows a user to use debug functions in the staff client', 'ppl', 'description' ));
8867
8868 UPDATE asset.call_number SET id = id WHERE deleted IS FALSE OR deleted = FALSE;
8869
8870 -- 0529
8871 INSERT INTO config.org_unit_setting_type 
8872 ( name, label, description, datatype ) VALUES 
8873 ( 'circ.user_merge.delete_addresses', 
8874   'Circ:  Patron Merge Address Delete', 
8875   'Delete address(es) of subordinate user(s) in a patron merge', 
8876    'bool'
8877 );
8878
8879 INSERT INTO config.org_unit_setting_type 
8880 ( name, label, description, datatype ) VALUES 
8881 ( 'circ.user_merge.delete_cards', 
8882   'Circ: Patron Merge Barcode Delete', 
8883   'Delete barcode(s) of subordinate user(s) in a patron merge', 
8884   'bool'
8885 );
8886
8887 INSERT INTO config.org_unit_setting_type 
8888 ( name, label, description, datatype ) VALUES 
8889 ( 'circ.user_merge.deactivate_cards', 
8890   'Circ:  Patron Merge Deactivate Card', 
8891   'Mark barcode(s) of subordinate user(s) in a patron merge as inactive', 
8892   'bool'
8893 );
8894
8895 DROP TRIGGER IF EXISTS mat_summary_add_tgr ON money.cash_payment;
8896 DROP TRIGGER IF EXISTS mat_summary_upd_tgr ON money.cash_payment;
8897 DROP TRIGGER IF EXISTS mat_summary_del_tgr ON money.cash_payment;
8898
8899 CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('cash_payment');
8900 CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('cash_payment');
8901 CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('cash_payment');
8902
8903 DROP TRIGGER IF EXISTS mat_summary_add_tgr ON money.check_payment;
8904 DROP TRIGGER IF EXISTS mat_summary_upd_tgr ON money.check_payment;
8905 DROP TRIGGER IF EXISTS mat_summary_del_tgr ON money.check_payment;
8906
8907 CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('check_payment');
8908 CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('check_payment');
8909 CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('check_payment');
8910
8911
8912 UPDATE  metabib.record_attr
8913   SET   attrs = attrs || asort
8914   FROM  (SELECT record,
8915                 HSTORE('authorsort',FIRST(value)) AS asort
8916           FROM  metabib.full_rec
8917           WHERE tag like '1%'
8918         GROUP BY 1) x
8919   WHERE x.record = metabib.record_attr.id;
8920
8921 UPDATE  metabib.record_attr
8922   SET   attrs = attrs || tsort
8923   FROM  (SELECT record,
8924                 HSTORE('titlesort',FIRST(value)) AS tsort
8925           FROM  metabib.full_rec
8926           WHERE tag = 'tnf'
8927         GROUP BY 1) x
8928   WHERE x.record = metabib.record_attr.id;
8929
8930
8931