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