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