]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/2.0-2.1-upgrade-db.sql
Stamping upgrade scripts for LP#818740
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 2.0-2.1-upgrade-db.sql
1 BEGIN;
2
3 -- 0425
4 ALTER TABLE permission.grp_tree
5         ADD COLUMN hold_priority INT NOT NULL DEFAULT 0;
6
7 -- 0430
8 ALTER TABLE config.hold_matrix_matchpoint ADD COLUMN strict_ou_match BOOL NOT NULL DEFAULT FALSE;
9
10 -- 0498
11 -- Rather than polluting the public schema with general Evergreen
12 -- functions, carve out a dedicated schema
13 CREATE SCHEMA evergreen;
14
15 -- Replace all uses of PostgreSQL's built-in LOWER() function with
16 -- a more locale-savvy PLPERLU evergreen.lowercase() function
17 CREATE OR REPLACE FUNCTION evergreen.lowercase( TEXT ) RETURNS TEXT AS $$
18     return lc(shift);
19 $$ LANGUAGE PLPERLU STRICT IMMUTABLE;
20
21 -- 0500
22 CREATE OR REPLACE FUNCTION evergreen.change_db_setting(setting_name TEXT, settings TEXT[]) RETURNS VOID AS $$
23 BEGIN
24 EXECUTE 'ALTER DATABASE ' || quote_ident(current_database()) || ' SET ' || quote_ident(setting_name) || ' = ' || array_to_string(settings, ',');
25 END;
26
27 $$ LANGUAGE plpgsql;
28
29 -- 0501
30 SELECT evergreen.change_db_setting('search_path', ARRAY['evergreen','public','pg_catalog']);
31
32 -- Fix function breakage due to short search path
33 CREATE OR REPLACE FUNCTION evergreen.force_unicode_normal_form(string TEXT, form TEXT) RETURNS TEXT AS $func$
34 use Unicode::Normalize 'normalize';
35 return normalize($_[1],$_[0]); # reverse the params
36 $func$ LANGUAGE PLPERLU;
37
38 CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
39 BEGIN
40     NEW.value := force_unicode_normal_form(NEW.value,'NFC');
41     RETURN NEW;
42 END;
43 $$ LANGUAGE PLPGSQL;
44
45 CREATE OR REPLACE FUNCTION evergreen.xml_escape(str TEXT) RETURNS text AS $$
46     SELECT REPLACE(REPLACE(REPLACE($1,
47        '&', '&'),
48        '<', '&lt;'),
49        '>', '&gt;');
50 $$ LANGUAGE SQL IMMUTABLE;
51
52 CREATE OR REPLACE FUNCTION evergreen.maintain_901 () RETURNS TRIGGER AS $func$
53 DECLARE
54     use_id_for_tcn BOOLEAN;
55 BEGIN
56     -- Remove any existing 901 fields before we insert the authoritative one
57     NEW.marc := REGEXP_REPLACE(NEW.marc, E'<datafield[^>]*?tag="901".+?</datafield>', '', 'g');
58
59     IF TG_TABLE_SCHEMA = 'biblio' THEN
60         -- Set TCN value to record ID?
61         SELECT enabled FROM config.global_flag INTO use_id_for_tcn
62             WHERE name = 'cat.bib.use_id_for_tcn';
63
64         IF use_id_for_tcn = 't' THEN
65             NEW.tcn_value := NEW.id;
66         END IF;
67
68         NEW.marc := REGEXP_REPLACE(
69             NEW.marc,
70             E'(</(?:[^:]*?:)?record>)',
71             E'<datafield tag="901" ind1=" " ind2=" ">' ||
72                 '<subfield code="a">' || evergreen.xml_escape(NEW.tcn_value) || E'</subfield>' ||
73                 '<subfield code="b">' || evergreen.xml_escape(NEW.tcn_source) || E'</subfield>' ||
74                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
75                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
76                 CASE WHEN NEW.owner IS NOT NULL THEN '<subfield code="o">' || NEW.owner || E'</subfield>' ELSE '' END ||
77                 CASE WHEN NEW.share_depth IS NOT NULL THEN '<subfield code="d">' || NEW.share_depth || E'</subfield>' ELSE '' END ||
78              E'</datafield>\\1'
79         );
80     ELSIF TG_TABLE_SCHEMA = 'authority' THEN
81         NEW.marc := REGEXP_REPLACE(
82             NEW.marc,
83             E'(</(?:[^:]*?:)?record>)',
84             E'<datafield tag="901" ind1=" " ind2=" ">' ||
85                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
86                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
87              E'</datafield>\\1'
88         );
89     ELSIF TG_TABLE_SCHEMA = 'serial' 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                 '<subfield code="o">' || NEW.owning_lib || E'</subfield>' ||
97                 CASE WHEN NEW.record IS NOT NULL THEN '<subfield code="r">' || NEW.record || E'</subfield>' ELSE '' END ||
98              E'</datafield>\\1'
99         );
100     ELSE
101         NEW.marc := REGEXP_REPLACE(
102             NEW.marc,
103             E'(</(?:[^:]*?:)?record>)',
104             E'<datafield tag="901" ind1=" " ind2=" ">' ||
105                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
106                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
107              E'</datafield>\\1'
108         );
109     END IF;
110
111     RETURN NEW;
112 END;
113 $func$ LANGUAGE PLPGSQL;
114
115 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;
116
117 CREATE OR REPLACE FUNCTION evergreen.lpad_number_substrings( TEXT, TEXT, INT ) RETURNS TEXT AS $$
118     my $string = shift;
119     my $pad = shift;
120     my $len = shift;
121     my $find = $len - 1;
122
123     while ($string =~ /(?:^|\D)(\d{1,$find})(?:$|\D)/) {
124         my $padded = $1;
125         $padded = $pad x ($len - length($padded)) . $padded;
126         $string =~ s/$1/$padded/sg;
127     }
128
129     return $string;
130 $$ LANGUAGE PLPERLU;
131
132 -- 0477
133 ALTER TABLE config.hard_due_date DROP CONSTRAINT hard_due_date_name_check;
134
135 -- 0478
136 CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
137
138     use strict;
139     use Unicode::Normalize;
140     use Encode;
141
142     my $str = decode_utf8(shift);
143     my $sf = shift;
144
145     # Apply NACO normalization to input string; based on
146     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
147     #
148     # Note that unlike a strict reading of the NACO normalization rules,
149     # output is returned as lowercase instead of uppercase for compatibility
150     # with previous versions of the Evergreen naco_normalize routine.
151
152     # Convert to upper-case first; even though final output will be lowercase, doing this will
153     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
154     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
155     $str = uc $str;
156
157     # remove non-filing strings
158     $str =~ s/\x{0098}.*?\x{009C}//g;
159
160     $str = NFKD($str);
161
162     # additional substitutions - 3.6.
163     $str =~ s/\x{00C6}/AE/g;
164     $str =~ s/\x{00DE}/TH/g;
165     $str =~ s/\x{0152}/OE/g;
166     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
167
168     # transformations based on Unicode category codes
169     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
170
171         if ($sf && $sf =~ /^a/o) {
172                 my $commapos = index($str, ',');
173                 if ($commapos > -1) {
174                         if ($commapos != length($str) - 1) {
175                 $str =~ s/,/\x07/; # preserve first comma
176                         }
177                 }
178         }
179
180     # since we've stripped out the control characters, we can now
181     # use a few as placeholders temporarily
182     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
183     $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;
184     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
185
186     # decimal digits
187     $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/;
188
189     # intentionally skipping step 8 of the NACO algorithm; if the string
190     # gets normalized away, that's fine.
191
192     # leading and trailing spaces
193     $str =~ s/\s+/ /g;
194     $str =~ s/^\s+//;
195     $str =~ s/\s+$//g;
196
197     return lc $str;
198 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
199
200 -- 0479
201 CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
202     WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
203             SELECT $1, 0
204         UNION
205             SELECT pgt.parent, gad.distance+1
206             FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON pgt.id = gad.id
207             WHERE pgt.parent IS NOT NULL
208     )
209     SELECT * FROM grp_ancestors_distance;
210 $$ LANGUAGE SQL STABLE;
211
212 CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
213     WITH RECURSIVE grp_descendants_distance(id, distance) AS (
214             SELECT $1, 0
215         UNION
216             SELECT pgt.id, gdd.distance+1
217             FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON pgt.parent = gdd.id
218     )
219     SELECT * FROM grp_descendants_distance;
220 $$ LANGUAGE SQL STABLE;
221
222 CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
223     WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
224             SELECT $1, 0
225         UNION
226             SELECT ou.parent_ou, ouad.distance+1
227             FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON ou.id = ouad.id
228             WHERE ou.parent_ou IS NOT NULL
229     )
230     SELECT * FROM org_unit_ancestors_distance;
231 $$ LANGUAGE SQL STABLE;
232
233 CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
234     WITH RECURSIVE org_unit_descendants_distance(id, distance) AS (
235             SELECT $1, 0
236         UNION
237             SELECT ou.id, oudd.distance+1
238             FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON ou.parent_ou = oudd.id
239     )
240     SELECT * FROM org_unit_descendants_distance;
241 $$ LANGUAGE SQL STABLE;
242
243 ALTER TABLE config.circ_matrix_matchpoint
244     ADD COLUMN user_home_ou         INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED;
245
246 CREATE TABLE config.circ_matrix_weights (
247     id                      SERIAL  PRIMARY KEY,
248     name                    TEXT    NOT NULL UNIQUE,
249     org_unit                NUMERIC(6,2)   NOT NULL,
250     grp                     NUMERIC(6,2)   NOT NULL,
251     circ_modifier           NUMERIC(6,2)   NOT NULL,
252     marc_type               NUMERIC(6,2)   NOT NULL,
253     marc_form               NUMERIC(6,2)   NOT NULL,
254     marc_vr_format          NUMERIC(6,2)   NOT NULL,
255     copy_circ_lib           NUMERIC(6,2)   NOT NULL,
256     copy_owning_lib         NUMERIC(6,2)   NOT NULL,
257     user_home_ou            NUMERIC(6,2)   NOT NULL,
258     ref_flag                NUMERIC(6,2)   NOT NULL,
259     juvenile_flag           NUMERIC(6,2)   NOT NULL,
260     is_renewal              NUMERIC(6,2)   NOT NULL,
261     usr_age_lower_bound     NUMERIC(6,2)   NOT NULL,
262     usr_age_upper_bound     NUMERIC(6,2)   NOT NULL
263 );
264
265 CREATE TABLE config.hold_matrix_weights (
266     id                      SERIAL  PRIMARY KEY,
267     name                    TEXT    NOT NULL UNIQUE,
268     user_home_ou            NUMERIC(6,2)   NOT NULL,
269     request_ou              NUMERIC(6,2)   NOT NULL,
270     pickup_ou               NUMERIC(6,2)   NOT NULL,
271     item_owning_ou          NUMERIC(6,2)   NOT NULL,
272     item_circ_ou            NUMERIC(6,2)   NOT NULL,
273     usr_grp                 NUMERIC(6,2)   NOT NULL,
274     requestor_grp           NUMERIC(6,2)   NOT NULL,
275     circ_modifier           NUMERIC(6,2)   NOT NULL,
276     marc_type               NUMERIC(6,2)   NOT NULL,
277     marc_form               NUMERIC(6,2)   NOT NULL,
278     marc_vr_format          NUMERIC(6,2)   NOT NULL,
279     juvenile_flag           NUMERIC(6,2)   NOT NULL,
280     ref_flag                NUMERIC(6,2)   NOT NULL
281 );
282
283 CREATE TABLE config.weight_assoc (
284     id                      SERIAL  PRIMARY KEY,
285     active                  BOOL    NOT NULL,
286     org_unit                INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
287     circ_weights            INT     REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
288     hold_weights            INT     REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
289 );
290 CREATE UNIQUE INDEX cwa_one_active_per_ou ON config.weight_assoc (org_unit) WHERE active;
291
292 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 
293     ('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),
294     ('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),
295     ('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),
296     ('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);
297
298 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
299     ('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),
300     ('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),
301     ('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),
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);
303
304 INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES
305     (true, 1, 1, 1);
306
307 -- 0480
308 CREATE OR REPLACE FUNCTION actor.usr_purge_data(
309         src_usr  IN INTEGER,
310         specified_dest_usr IN INTEGER
311 ) RETURNS VOID AS $$
312 DECLARE
313         suffix TEXT;
314         renamable_row RECORD;
315         dest_usr INTEGER;
316 BEGIN
317
318         IF specified_dest_usr IS NULL THEN
319                 dest_usr := 1; -- Admin user on stock installs
320         ELSE
321                 dest_usr := specified_dest_usr;
322         END IF;
323
324         UPDATE actor.usr SET
325                 active = FALSE,
326                 card = NULL,
327                 mailing_address = NULL,
328                 billing_address = NULL
329         WHERE id = src_usr;
330
331         -- acq.*
332         UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
333         UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
334         UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
335         UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
336         UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
337         UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
338         DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
339
340         -- Update with a rename to avoid collisions
341         FOR renamable_row in
342                 SELECT id, name
343                 FROM   acq.picklist
344                 WHERE  owner = src_usr
345         LOOP
346                 suffix := ' (' || src_usr || ')';
347                 LOOP
348                         BEGIN
349                                 UPDATE  acq.picklist
350                                 SET     owner = dest_usr, name = name || suffix
351                                 WHERE   id = renamable_row.id;
352                         EXCEPTION WHEN unique_violation THEN
353                                 suffix := suffix || ' ';
354                                 CONTINUE;
355                         END;
356                         EXIT;
357                 END LOOP;
358         END LOOP;
359
360         UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
361         UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
362         UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
363         UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
364         UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
365         UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
366         UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
367         UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
368
369         -- action.*
370         DELETE FROM action.circulation WHERE usr = src_usr;
371         UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
372         UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
373         UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
374         UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
375         UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
376         DELETE FROM action.hold_request WHERE usr = src_usr;
377         UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
378         UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
379         DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
380         UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
381         DELETE FROM action.survey_response WHERE usr = src_usr;
382         UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
383
384         -- actor.*
385         DELETE FROM actor.card WHERE usr = src_usr;
386         DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
387
388         -- The following update is intended to avoid transient violations of a foreign
389         -- key constraint, whereby actor.usr_address references itself.  It may not be
390         -- necessary, but it does no harm.
391         UPDATE actor.usr_address SET replaces = NULL
392                 WHERE usr = src_usr AND replaces IS NOT NULL;
393         DELETE FROM actor.usr_address WHERE usr = src_usr;
394         DELETE FROM actor.usr_note WHERE usr = src_usr;
395         UPDATE actor.usr_note SET creator = dest_usr WHERE creator = src_usr;
396         DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
397         UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
398         DELETE FROM actor.usr_setting WHERE usr = src_usr;
399         DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
400         UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
401
402         -- asset.*
403         UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
404         UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
405         UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
406         UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
407         UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
408         UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
409
410         -- auditor.*
411         DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
412         DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
413         UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
414         UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
415         UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
416         UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
417         UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
418         UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
419
420         -- biblio.*
421         UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
422         UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
423         UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
424         UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
425
426         -- container.*
427         -- Update buckets with a rename to avoid collisions
428         FOR renamable_row in
429                 SELECT id, name
430                 FROM   container.biblio_record_entry_bucket
431                 WHERE  owner = src_usr
432         LOOP
433                 suffix := ' (' || src_usr || ')';
434                 LOOP
435                         BEGIN
436                                 UPDATE  container.biblio_record_entry_bucket
437                                 SET     owner = dest_usr, name = name || suffix
438                                 WHERE   id = renamable_row.id;
439                         EXCEPTION WHEN unique_violation THEN
440                                 suffix := suffix || ' ';
441                                 CONTINUE;
442                         END;
443                         EXIT;
444                 END LOOP;
445         END LOOP;
446
447         FOR renamable_row in
448                 SELECT id, name
449                 FROM   container.call_number_bucket
450                 WHERE  owner = src_usr
451         LOOP
452                 suffix := ' (' || src_usr || ')';
453                 LOOP
454                         BEGIN
455                                 UPDATE  container.call_number_bucket
456                                 SET     owner = dest_usr, name = name || suffix
457                                 WHERE   id = renamable_row.id;
458                         EXCEPTION WHEN unique_violation THEN
459                                 suffix := suffix || ' ';
460                                 CONTINUE;
461                         END;
462                         EXIT;
463                 END LOOP;
464         END LOOP;
465
466         FOR renamable_row in
467                 SELECT id, name
468                 FROM   container.copy_bucket
469                 WHERE  owner = src_usr
470         LOOP
471                 suffix := ' (' || src_usr || ')';
472                 LOOP
473                         BEGIN
474                                 UPDATE  container.copy_bucket
475                                 SET     owner = dest_usr, name = name || suffix
476                                 WHERE   id = renamable_row.id;
477                         EXCEPTION WHEN unique_violation THEN
478                                 suffix := suffix || ' ';
479                                 CONTINUE;
480                         END;
481                         EXIT;
482                 END LOOP;
483         END LOOP;
484
485         FOR renamable_row in
486                 SELECT id, name
487                 FROM   container.user_bucket
488                 WHERE  owner = src_usr
489         LOOP
490                 suffix := ' (' || src_usr || ')';
491                 LOOP
492                         BEGIN
493                                 UPDATE  container.user_bucket
494                                 SET     owner = dest_usr, name = name || suffix
495                                 WHERE   id = renamable_row.id;
496                         EXCEPTION WHEN unique_violation THEN
497                                 suffix := suffix || ' ';
498                                 CONTINUE;
499                         END;
500                         EXIT;
501                 END LOOP;
502         END LOOP;
503
504         DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
505
506         -- money.*
507         DELETE FROM money.billable_xact WHERE usr = src_usr;
508         DELETE FROM money.collections_tracker WHERE usr = src_usr;
509         UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
510
511         -- permission.*
512         DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
513         DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
514         DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
515         DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
516
517         -- reporter.*
518         -- Update with a rename to avoid collisions
519         BEGIN
520                 FOR renamable_row in
521                         SELECT id, name
522                         FROM   reporter.output_folder
523                         WHERE  owner = src_usr
524                 LOOP
525                         suffix := ' (' || src_usr || ')';
526                         LOOP
527                                 BEGIN
528                                         UPDATE  reporter.output_folder
529                                         SET     owner = dest_usr, name = name || suffix
530                                         WHERE   id = renamable_row.id;
531                                 EXCEPTION WHEN unique_violation THEN
532                                         suffix := suffix || ' ';
533                                         CONTINUE;
534                                 END;
535                                 EXIT;
536                         END LOOP;
537                 END LOOP;
538         EXCEPTION WHEN undefined_table THEN
539                 -- do nothing
540         END;
541
542         BEGIN
543                 UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
544         EXCEPTION WHEN undefined_table THEN
545                 -- do nothing
546         END;
547
548         -- Update with a rename to avoid collisions
549         BEGIN
550                 FOR renamable_row in
551                         SELECT id, name
552                         FROM   reporter.report_folder
553                         WHERE  owner = src_usr
554                 LOOP
555                         suffix := ' (' || src_usr || ')';
556                         LOOP
557                                 BEGIN
558                                         UPDATE  reporter.report_folder
559                                         SET     owner = dest_usr, name = name || suffix
560                                         WHERE   id = renamable_row.id;
561                                 EXCEPTION WHEN unique_violation THEN
562                                         suffix := suffix || ' ';
563                                         CONTINUE;
564                                 END;
565                                 EXIT;
566                         END LOOP;
567                 END LOOP;
568         EXCEPTION WHEN undefined_table THEN
569                 -- do nothing
570         END;
571
572         BEGIN
573                 UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
574         EXCEPTION WHEN undefined_table THEN
575                 -- do nothing
576         END;
577
578         BEGIN
579                 UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
580         EXCEPTION WHEN undefined_table THEN
581                 -- do nothing
582         END;
583
584         -- Update with a rename to avoid collisions
585         BEGIN
586                 FOR renamable_row in
587                         SELECT id, name
588                         FROM   reporter.template_folder
589                         WHERE  owner = src_usr
590                 LOOP
591                         suffix := ' (' || src_usr || ')';
592                         LOOP
593                                 BEGIN
594                                         UPDATE  reporter.template_folder
595                                         SET     owner = dest_usr, name = name || suffix
596                                         WHERE   id = renamable_row.id;
597                                 EXCEPTION WHEN unique_violation THEN
598                                         suffix := suffix || ' ';
599                                         CONTINUE;
600                                 END;
601                                 EXIT;
602                         END LOOP;
603                 END LOOP;
604         EXCEPTION WHEN undefined_table THEN
605         -- do nothing
606         END;
607
608         -- vandelay.*
609         -- Update with a rename to avoid collisions
610         FOR renamable_row in
611                 SELECT id, name
612                 FROM   vandelay.queue
613                 WHERE  owner = src_usr
614         LOOP
615                 suffix := ' (' || src_usr || ')';
616                 LOOP
617                         BEGIN
618                                 UPDATE  vandelay.queue
619                                 SET     owner = dest_usr, name = name || suffix
620                                 WHERE   id = renamable_row.id;
621                         EXCEPTION WHEN unique_violation THEN
622                                 suffix := suffix || ' ';
623                                 CONTINUE;
624                         END;
625                         EXIT;
626                 END LOOP;
627         END LOOP;
628
629 END;
630 $$ LANGUAGE plpgsql;
631
632 -- 0482
633 -- Drop old (non-functional) constraints
634
635 ALTER TABLE config.circ_matrix_matchpoint
636     DROP CONSTRAINT ep_once_per_grp_loc_mod_marc;
637
638 ALTER TABLE config.hold_matrix_matchpoint
639     DROP CONSTRAINT hous_once_per_grp_loc_mod_marc;
640
641 -- Clean up tables before making normalized index
642
643 CREATE OR REPLACE FUNCTION action.cleanup_matrix_matchpoints() RETURNS void AS $func$
644 DECLARE
645     temp_row    RECORD;
646 BEGIN
647     -- Circ Matrix
648     FOR temp_row IN
649         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
650         FROM config.circ_matrix_matchpoint
651         WHERE active
652         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
653         HAVING COUNT(id) > 1 LOOP
654
655         UPDATE config.circ_matrix_matchpoint SET active=false
656             WHERE id > temp_row.firstrow
657                 AND org_unit = temp_row.org_unit
658                 AND grp = temp_row.grp
659                 AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
660                 AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
661                 AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
662                 AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
663                 AND copy_circ_lib       IS NOT DISTINCT FROM temp_row.copy_circ_lib
664                 AND copy_owning_lib     IS NOT DISTINCT FROM temp_row.copy_owning_lib
665                 AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
666                 AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag
667                 AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
668                 AND is_renewal          IS NOT DISTINCT FROM temp_row.is_renewal
669                 AND usr_age_lower_bound IS NOT DISTINCT FROM temp_row.usr_age_lower_bound
670                 AND usr_age_upper_bound IS NOT DISTINCT FROM temp_row.usr_age_upper_bound;
671     END LOOP;
672
673     -- Hold Matrix
674     FOR temp_row IN
675         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
676         FROM config.hold_matrix_matchpoint
677         WHERE active
678         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
679         HAVING COUNT(id) > 1 LOOP
680
681         UPDATE config.hold_matrix_matchpoint SET active=false
682             WHERE id > temp_row.firstrow
683                 AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
684                 AND request_ou          IS NOT DISTINCT FROM temp_row.request_ou
685                 AND pickup_ou           IS NOT DISTINCT FROM temp_row.pickup_ou
686                 AND item_owning_ou      IS NOT DISTINCT FROM temp_row.item_owning_ou
687                 AND item_circ_ou        IS NOT DISTINCT FROM temp_row.item_circ_ou
688                 AND usr_grp             IS NOT DISTINCT FROM temp_row.usr_grp
689                 AND requestor_grp       IS NOT DISTINCT FROM temp_row.requestor_grp
690                 AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
691                 AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
692                 AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
693                 AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
694                 AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
695                 AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag;
696     END LOOP;
697 END;
698 $func$ LANGUAGE plpgsql;
699
700 SELECT action.cleanup_matrix_matchpoints();
701
702 DROP FUNCTION IF EXISTS action.cleanup_matrix_matchpoints();
703
704 -- Create Normalized indexes
705
706 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;
707
708 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;
709
710 -- 0484
711 DROP FUNCTION asset.metarecord_copy_count ( INT, BIGINT, BOOL );
712 DROP FUNCTION asset.record_copy_count ( INT, BIGINT, BOOL );
713
714 DROP FUNCTION asset.opac_ou_record_copy_count (INT, BIGINT);
715 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$
716 DECLARE
717     ans RECORD;
718     trans INT;
719 BEGIN
720     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;
721
722     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
723         RETURN QUERY
724         SELECT  ans.depth,
725                 ans.id,
726                 COUNT( av.id ),
727                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
728                 COUNT( av.id ),
729                 trans
730           FROM  
731                 actor.org_unit_descendants(ans.id) d
732                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
733                 JOIN asset.copy cp ON (cp.id = av.copy_id)
734           GROUP BY 1,2,6;
735
736         IF NOT FOUND THEN
737             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
738         END IF;
739
740     END LOOP;
741
742     RETURN;
743 END;
744 $f$ LANGUAGE PLPGSQL;
745
746 DROP FUNCTION asset.opac_lasso_record_copy_count (INT, BIGINT);
747 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$
748 DECLARE
749     ans RECORD;
750     trans INT;
751 BEGIN
752     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;
753
754     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
755         RETURN QUERY
756         SELECT  -1,
757                 ans.id,
758                 COUNT( av.id ),
759                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
760                 COUNT( av.id ),
761                 trans
762           FROM  
763                 actor.org_unit_descendants(ans.id) d
764                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
765                 JOIN asset.copy cp ON (cp.id = av.copy_id)
766           GROUP BY 1,2,6;
767
768         IF NOT FOUND THEN
769             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
770         END IF;
771
772     END LOOP;
773
774     RETURN;
775 END;
776 $f$ LANGUAGE PLPGSQL;
777
778 DROP FUNCTION asset.staff_ou_record_copy_count (INT, BIGINT);
779
780 DROP FUNCTION asset.staff_lasso_record_copy_count (INT, BIGINT);
781
782 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$
783 BEGIN
784     IF staff IS TRUE THEN
785         IF place > 0 THEN
786             RETURN QUERY SELECT * FROM asset.staff_ou_record_copy_count( place, rid );
787         ELSE
788             RETURN QUERY SELECT * FROM asset.staff_lasso_record_copy_count( -place, rid );
789         END IF;
790     ELSE
791         IF place > 0 THEN
792             RETURN QUERY SELECT * FROM asset.opac_ou_record_copy_count( place, rid );
793         ELSE
794             RETURN QUERY SELECT * FROM asset.opac_lasso_record_copy_count( -place, rid );
795         END IF;
796     END IF;
797
798     RETURN;
799 END;
800 $f$ LANGUAGE PLPGSQL;
801
802 DROP FUNCTION asset.opac_ou_metarecord_copy_count (INT, BIGINT);
803 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$
804 DECLARE
805     ans RECORD;
806     trans INT;
807 BEGIN
808     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;
809
810     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
811         RETURN QUERY
812         SELECT  ans.depth,
813                 ans.id,
814                 COUNT( av.id ),
815                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
816                 COUNT( av.id ),
817                 trans
818           FROM
819                 actor.org_unit_descendants(ans.id) d
820                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
821                 JOIN asset.copy cp ON (cp.id = av.copy_id)
822                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
823           GROUP BY 1,2,6;
824
825         IF NOT FOUND THEN
826             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
827         END IF;
828
829     END LOOP;
830
831     RETURN;
832 END;
833 $f$ LANGUAGE PLPGSQL;
834
835 DROP FUNCTION asset.opac_lasso_metarecord_copy_count (INT, BIGINT);
836 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$
837 DECLARE
838     ans RECORD;
839     trans INT;
840 BEGIN
841     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;
842
843     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
844         RETURN QUERY
845         SELECT  -1,
846                 ans.id,
847                 COUNT( av.id ),
848                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
849                 COUNT( av.id ),
850                 trans
851           FROM
852                 actor.org_unit_descendants(ans.id) d
853                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
854                 JOIN asset.copy cp ON (cp.id = av.copy_id)
855                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
856           GROUP BY 1,2,6;
857
858         IF NOT FOUND THEN
859             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
860         END IF;
861
862     END LOOP;
863
864     RETURN;
865 END;
866 $f$ LANGUAGE PLPGSQL;
867
868 DROP FUNCTION asset.staff_lasso_metarecord_copy_count (INT, BIGINT);
869
870 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$
871 BEGIN
872     IF staff IS TRUE THEN
873         IF place > 0 THEN
874             RETURN QUERY SELECT * FROM asset.staff_ou_metarecord_copy_count( place, rid );
875         ELSE
876             RETURN QUERY SELECT * FROM asset.staff_lasso_metarecord_copy_count( -place, rid );
877         END IF;
878     ELSE
879         IF place > 0 THEN
880             RETURN QUERY SELECT * FROM asset.opac_ou_metarecord_copy_count( place, rid );
881         ELSE
882             RETURN QUERY SELECT * FROM asset.opac_lasso_metarecord_copy_count( -place, rid );
883         END IF;
884     END IF;
885
886     RETURN;
887 END;
888 $f$ LANGUAGE PLPGSQL;
889
890 -- 0485
891 CREATE OR REPLACE VIEW reporter.simple_record AS
892 SELECT  r.id,
893         s.metarecord,
894         r.fingerprint,
895         r.quality,
896         r.tcn_source,
897         r.tcn_value,
898         title.value AS title,
899         uniform_title.value AS uniform_title,
900         author.value AS author,
901         publisher.value AS publisher,
902         SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
903         series_title.value AS series_title,
904         series_statement.value AS series_statement,
905         summary.value AS summary,
906         ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
907         ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
908         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
909         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
910         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
911         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
912         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
913         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
914   FROM  biblio.record_entry r
915         JOIN metabib.metarecord_source_map s ON (s.source = r.id)
916         LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
917         LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
918         LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
919         LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
920         LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
921         LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
922         LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
923         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')
924         LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
925         LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
926   GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
927
928 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
929 SELECT  r.id,
930     r.fingerprint,
931     r.quality,
932     r.tcn_source,
933     r.tcn_value,
934     FIRST(title.value) AS title,
935     FIRST(author.value) AS author,
936     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT publisher.value), ', ') AS publisher,
937     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$) ), ', ') AS pubdate,
938     ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
939     ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn
940   FROM  biblio.record_entry r
941     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
942     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
943     LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
944     LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
945     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
946     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
947   GROUP BY 1,2,3,4,5;
948
949 -- 0486
950 ALTER TABLE money.credit_card_payment ADD COLUMN cc_order_number TEXT;
951
952 -- 0487
953 -- Circ matchpoint table changes
954 ALTER TABLE config.circ_matrix_matchpoint
955     ALTER COLUMN circulate DROP NOT NULL, -- Fallthrough enable
956     ALTER COLUMN circulate DROP DEFAULT, -- Stop defaulting to true to enable default to fallthrough
957     ALTER COLUMN duration_rule DROP NOT NULL, -- Fallthrough enable
958     ALTER COLUMN recurring_fine_rule DROP NOT NULL, -- Fallthrough enable
959     ALTER COLUMN max_fine_rule DROP NOT NULL, -- Fallthrough enable
960     ADD COLUMN renewals INT; -- Renewals override
961
962 -- Changing return types requires explicit dropping of old versions
963 DROP FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
964 DROP FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
965 DROP FUNCTION action.item_user_circ_test( INT, BIGINT, INT );
966 DROP FUNCTION action.item_user_renew_test( INT, BIGINT, INT );
967
968 -- New return types
969 CREATE TYPE action.found_circ_matrix_matchpoint AS ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] );
970
971 -- Helper function - For manual calling, it can be easier to pass in IDs instead of objects
972 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$
973 DECLARE
974     item_object asset.copy%ROWTYPE;
975     user_object actor.usr%ROWTYPE;
976 BEGIN
977     SELECT INTO item_object * FROM asset.copy   WHERE id = match_item;
978     SELECT INTO user_object * FROM actor.usr    WHERE id = match_user;
979
980     RETURN QUERY SELECT * FROM action.find_circ_matrix_matchpoint( context_ou, item_object, user_object, renewal );
981 END;
982 $func$ LANGUAGE plpgsql;
983
984 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 );
985
986 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$
987 DECLARE
988     user_object             actor.usr%ROWTYPE;
989     standing_penalty        config.standing_penalty%ROWTYPE;
990     item_object             asset.copy%ROWTYPE;
991     item_status_object      config.copy_status%ROWTYPE;
992     item_location_object    asset.copy_location%ROWTYPE;
993     result                  action.circ_matrix_test_result;
994     circ_test               action.found_circ_matrix_matchpoint;
995     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
996     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
997     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
998     hold_ratio              action.hold_stats%ROWTYPE;
999     penalty_type            TEXT;
1000     items_out               INT;
1001     context_org_list        INT[];
1002     done                    BOOL := FALSE;
1003 BEGIN
1004     -- Assume success unless we hit a failure condition
1005     result.success := TRUE;
1006
1007     -- Fail if the user is BARRED
1008     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
1009
1010     -- Fail if we couldn't find the user 
1011     IF user_object.id IS NULL THEN
1012         result.fail_part := 'no_user';
1013         result.success := FALSE;
1014         done := TRUE;
1015         RETURN NEXT result;
1016         RETURN;
1017     END IF;
1018
1019     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
1020
1021     -- Fail if we couldn't find the item 
1022     IF item_object.id IS NULL THEN
1023         result.fail_part := 'no_item';
1024         result.success := FALSE;
1025         done := TRUE;
1026         RETURN NEXT result;
1027         RETURN;
1028     END IF;
1029
1030     IF user_object.barred IS TRUE THEN
1031         result.fail_part := 'actor.usr.barred';
1032         result.success := FALSE;
1033         done := TRUE;
1034         RETURN NEXT result;
1035     END IF;
1036
1037     -- Fail if the item can't circulate
1038     IF item_object.circulate IS FALSE THEN
1039         result.fail_part := 'asset.copy.circulate';
1040         result.success := FALSE;
1041         done := TRUE;
1042         RETURN NEXT result;
1043     END IF;
1044
1045     -- Fail if the item isn't in a circulateable status on a non-renewal
1046     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
1047         result.fail_part := 'asset.copy.status';
1048         result.success := FALSE;
1049         done := TRUE;
1050         RETURN NEXT result;
1051     ELSIF renewal AND item_object.status <> 1 THEN
1052         result.fail_part := 'asset.copy.status';
1053         result.success := FALSE;
1054         done := TRUE;
1055         RETURN NEXT result;
1056     END IF;
1057
1058     -- Fail if the item can't circulate because of the shelving location
1059     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
1060     IF item_location_object.circulate IS FALSE THEN
1061         result.fail_part := 'asset.copy_location.circulate';
1062         result.success := FALSE;
1063         done := TRUE;
1064         RETURN NEXT result;
1065     END IF;
1066
1067     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
1068
1069     circ_matchpoint             := circ_test.matchpoint;
1070     result.matchpoint           := circ_matchpoint.id;
1071     result.circulate            := circ_matchpoint.circulate;
1072     result.duration_rule        := circ_matchpoint.duration_rule;
1073     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
1074     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
1075     result.hard_due_date        := circ_matchpoint.hard_due_date;
1076     result.renewals             := circ_matchpoint.renewals;
1077     result.buildrows            := circ_test.buildrows;
1078
1079     -- Fail if we couldn't find a matchpoint
1080     IF circ_test.success = false THEN
1081         result.fail_part := 'no_matchpoint';
1082         result.success := FALSE;
1083         done := TRUE;
1084         RETURN NEXT result;
1085         RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
1086     END IF;
1087
1088     -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
1089     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
1090
1091     IF renewal THEN
1092         penalty_type = '%RENEW%';
1093     ELSE
1094         penalty_type = '%CIRC%';
1095     END IF;
1096
1097     FOR standing_penalty IN
1098         SELECT  DISTINCT csp.*
1099           FROM  actor.usr_standing_penalty usp
1100                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1101           WHERE usr = match_user
1102                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1103                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1104                 AND csp.block_list LIKE penalty_type LOOP
1105
1106         result.fail_part := standing_penalty.name;
1107         result.success := FALSE;
1108         done := TRUE;
1109         RETURN NEXT result;
1110     END LOOP;
1111
1112     -- Fail if the test is set to hard non-circulating
1113     IF circ_matchpoint.circulate IS FALSE THEN
1114         result.fail_part := 'config.circ_matrix_test.circulate';
1115         result.success := FALSE;
1116         done := TRUE;
1117         RETURN NEXT result;
1118     END IF;
1119
1120     -- Fail if the total copy-hold ratio is too low
1121     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
1122         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
1123         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
1124             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
1125             result.success := FALSE;
1126             done := TRUE;
1127             RETURN NEXT result;
1128         END IF;
1129     END IF;
1130
1131     -- Fail if the available copy-hold ratio is too low
1132     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
1133         IF hold_ratio.hold_count IS NULL THEN
1134             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
1135         END IF;
1136         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
1137             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
1138             result.success := FALSE;
1139             done := TRUE;
1140             RETURN NEXT result;
1141         END IF;
1142     END IF;
1143
1144     -- Fail if the user has too many items with specific circ_modifiers checked out
1145     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
1146         SELECT  INTO items_out COUNT(*)
1147           FROM  action.circulation circ
1148             JOIN asset.copy cp ON (cp.id = circ.target_copy)
1149           WHERE circ.usr = match_user
1150                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
1151             AND circ.checkin_time IS NULL
1152             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
1153             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);
1154         IF items_out >= out_by_circ_mod.items_out THEN
1155             result.fail_part := 'config.circ_matrix_circ_mod_test';
1156             result.success := FALSE;
1157             done := TRUE;
1158             RETURN NEXT result;
1159         END IF;
1160     END LOOP;
1161
1162     -- If we passed everything, return the successful matchpoint id
1163     IF NOT done THEN
1164         RETURN NEXT result;
1165     END IF;
1166
1167     RETURN;
1168 END;
1169 $func$ LANGUAGE plpgsql;
1170
1171 CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
1172     SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
1173 $func$ LANGUAGE SQL;
1174
1175 CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
1176     SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
1177 $func$ LANGUAGE SQL;
1178
1179 -- 0490
1180 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$
1181 DECLARE         
1182     ans RECORD; 
1183     trans INT;
1184 BEGIN           
1185     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;
1186
1187     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
1188         RETURN QUERY
1189         SELECT  ans.depth,
1190                 ans.id,
1191                 COUNT( cp.id ),
1192                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1193                 COUNT( cp.id ),
1194                 trans
1195           FROM
1196                 actor.org_unit_descendants(ans.id) d
1197                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1198                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1199           GROUP BY 1,2,6;
1200
1201         IF NOT FOUND THEN
1202             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1203         END IF;
1204
1205     END LOOP;
1206
1207     RETURN;
1208 END;
1209 $f$ LANGUAGE PLPGSQL;
1210
1211 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$
1212 DECLARE
1213     ans RECORD;
1214     trans INT;
1215 BEGIN
1216     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;
1217
1218     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
1219         RETURN QUERY
1220         SELECT  -1,
1221                 ans.id,
1222                 COUNT( cp.id ),
1223                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1224                 COUNT( cp.id ),
1225                 trans
1226           FROM
1227                 actor.org_unit_descendants(ans.id) d
1228                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1229                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1230           GROUP BY 1,2,6;
1231
1232         IF NOT FOUND THEN
1233             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1234         END IF;
1235
1236     END LOOP;
1237
1238     RETURN;
1239 END;
1240 $f$ LANGUAGE PLPGSQL;
1241
1242 DROP FUNCTION asset.staff_ou_metarecord_copy_count (INT, BIGINT);
1243 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$
1244 DECLARE         
1245     ans RECORD; 
1246     trans INT;
1247 BEGIN
1248     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;
1249
1250     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
1251         RETURN QUERY
1252         SELECT  ans.depth,
1253                 ans.id,
1254                 COUNT( cp.id ),
1255                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1256                 COUNT( cp.id ),
1257                 trans
1258           FROM
1259                 actor.org_unit_descendants(ans.id) d
1260                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1261                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1262                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
1263           GROUP BY 1,2,6;
1264
1265         IF NOT FOUND THEN
1266             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1267         END IF;
1268
1269     END LOOP;
1270
1271     RETURN;
1272 END;
1273 $f$ LANGUAGE PLPGSQL;
1274
1275 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$
1276 DECLARE
1277     ans RECORD;
1278     trans INT;
1279 BEGIN
1280     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;
1281
1282     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
1283         RETURN QUERY
1284         SELECT  -1,
1285                 ans.id,
1286                 COUNT( cp.id ),
1287                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1288                 COUNT( cp.id ),
1289                 trans
1290           FROM
1291                 actor.org_unit_descendants(ans.id) d
1292                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1293                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1294                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
1295           GROUP BY 1,2,6;
1296
1297         IF NOT FOUND THEN
1298             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1299         END IF;
1300
1301     END LOOP;
1302
1303     RETURN;
1304 END;
1305 $f$ LANGUAGE PLPGSQL;
1306
1307
1308 -- 0493
1309 UPDATE config.org_unit_setting_type
1310     SET description = 'Amount of time before a hold expires at which point the patron should be alerted. Examples: "5 days", "1 hour"'
1311     WHERE label = 'Holds: Expire Alert Interval';
1312
1313 UPDATE config.org_unit_setting_type
1314     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"'
1315     WHERE label = 'Holds: Default Estimated Wait';
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 minimum estimated length of time to assume an item will be checked out. Examples: "1 week", "5 days"'
1319     WHERE label = 'Holds: Minimum Estimated Wait';
1320
1321 UPDATE config.org_unit_setting_type
1322     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"'
1323     WHERE label = 'Hold Shelf Status Delay';
1324
1325 -- 0494
1326 UPDATE config.metabib_field
1327     SET xpath = $$//mods32:mods/mods32:subject$$
1328     WHERE field_class = 'subject' AND name = 'complete';
1329
1330 UPDATE config.metabib_field
1331     SET xpath = $$//marc:datafield[@tag='099']$$
1332     WHERE field_class = 'identifier' AND name = 'bibcn';
1333
1334 -- 0495
1335 CREATE TABLE config.record_attr_definition (
1336     name        TEXT    PRIMARY KEY,
1337     label       TEXT    NOT NULL, -- I18N
1338     description TEXT,
1339     filter      BOOL    NOT NULL DEFAULT TRUE,  -- becomes QP filter if true
1340     sorter      BOOL    NOT NULL DEFAULT FALSE, -- becomes QP sort() axis if true
1341
1342 -- For pre-extracted fields. Takes the first occurance, uses naive subfield ordering
1343     tag         TEXT, -- LIKE format
1344     sf_list     TEXT, -- pile-o-values, like 'abcd' for a and b and c and d
1345
1346 -- This is used for both tag/sf and xpath entries
1347     joiner      TEXT,
1348
1349 -- For xpath-extracted attrs
1350     xpath       TEXT,
1351     format      TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1352     start_pos   INT,
1353     string_len  INT,
1354
1355 -- For fixed fields
1356     fixed_field TEXT, -- should exist in config.marc21_ff_pos_map.fixed_field
1357
1358 -- For phys-char fields
1359     phys_char_sf    INT REFERENCES config.marc21_physical_characteristic_subfield_map (id)
1360 );
1361
1362 CREATE TABLE config.record_attr_index_norm_map (
1363     id      SERIAL  PRIMARY KEY,
1364     attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1365     norm    INT     NOT NULL REFERENCES config.index_normalizer (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1366     params  TEXT,
1367     pos     INT     NOT NULL DEFAULT 0
1368 );
1369
1370 CREATE TABLE config.coded_value_map (
1371     id          SERIAL  PRIMARY KEY,
1372     ctype       TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1373     code        TEXT    NOT NULL,
1374     value       TEXT    NOT NULL,
1375     description TEXT
1376 );
1377
1378 -- record attributes
1379 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('alph','Alph','Alph');
1380 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('audience','Audn','Audn');
1381 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('bib_level','BLvl','BLvl');
1382 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('biog','Biog','Biog');
1383 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('conf','Conf','Conf');
1384 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('control_type','Ctrl','Ctrl');
1385 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ctry','Ctry','Ctry');
1386 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date1','Date1','Date1');
1387 INSERT INTO config.record_attr_definition (name,label,fixed_field,sorter,filter) values ('pubdate','Pub Date','Date1',TRUE,FALSE);
1388 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date2','Date2','Date2');
1389 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('cat_form','Desc','Desc');
1390 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('pub_status','DtSt','DtSt');
1391 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('enc_level','ELvl','ELvl');
1392 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('fest','Fest','Fest');
1393 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_form','Form','Form');
1394 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('gpub','GPub','GPub');
1395 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ills','Ills','Ills');
1396 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('indx','Indx','Indx');
1397 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_lang','Lang','Lang');
1398 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('lit_form','LitF','LitF');
1399 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('mrec','MRec','MRec');
1400 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ff_sl','S/L','S/L');
1401 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('type_mat','TMat','TMat');
1402 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_type','Type','Type');
1403 INSERT INTO config.record_attr_definition (name,label,phys_char_sf) values ('vr_format','Videorecording format',72);
1404 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('titlesort','Title',TRUE,FALSE,'tnf');
1405 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('authorsort','Author',TRUE,FALSE,'1%');
1406
1407 INSERT INTO config.coded_value_map (ctype,code,value,description)
1408     SELECT 'item_lang' AS ctype, code, value, NULL FROM config.language_map
1409         UNION
1410     SELECT 'bib_level' AS ctype, code, value, NULL FROM config.bib_level_map
1411         UNION
1412     SELECT 'item_form' AS ctype, code, value, NULL FROM config.item_form_map
1413         UNION
1414     SELECT 'item_type' AS ctype, code, value, NULL FROM config.item_type_map
1415         UNION
1416     SELECT 'lit_form' AS ctype, code, value, description FROM config.lit_form_map
1417         UNION
1418     SELECT 'audience' AS ctype, code, value, description FROM config.audience_map
1419         UNION
1420     SELECT 'vr_format' AS ctype, code, value, NULL FROM config.videorecording_format_map;
1421
1422 ALTER TABLE config.i18n_locale DROP CONSTRAINT i18n_locale_marc_code_fkey;
1423
1424 ALTER TABLE config.circ_matrix_matchpoint DROP CONSTRAINT circ_matrix_matchpoint_marc_form_fkey;
1425 ALTER TABLE config.circ_matrix_matchpoint DROP CONSTRAINT circ_matrix_matchpoint_marc_type_fkey;
1426 ALTER TABLE config.circ_matrix_matchpoint DROP CONSTRAINT circ_matrix_matchpoint_marc_vr_format_fkey;
1427
1428 ALTER TABLE config.hold_matrix_matchpoint DROP CONSTRAINT hold_matrix_matchpoint_marc_form_fkey;
1429 ALTER TABLE config.hold_matrix_matchpoint DROP CONSTRAINT hold_matrix_matchpoint_marc_type_fkey;
1430 ALTER TABLE config.hold_matrix_matchpoint DROP CONSTRAINT hold_matrix_matchpoint_marc_vr_format_fkey;
1431
1432 DROP TABLE config.language_map;
1433 DROP TABLE config.bib_level_map;
1434 DROP TABLE config.item_form_map;
1435 DROP TABLE config.item_type_map;
1436 DROP TABLE config.lit_form_map;
1437 DROP TABLE config.audience_map;
1438 DROP TABLE config.videorecording_format_map;
1439
1440 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;
1441 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;
1442 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;
1443 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;
1444 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;
1445 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;
1446 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;
1447
1448 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;
1449 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;
1450
1451 CREATE VIEW config.language_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_lang';
1452 CREATE VIEW config.bib_level_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'bib_level';
1453 CREATE VIEW config.item_form_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_form';
1454 CREATE VIEW config.item_type_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_type';
1455 CREATE VIEW config.lit_form_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'lit_form';
1456 CREATE VIEW config.audience_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'audience';
1457 CREATE VIEW config.videorecording_format_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'vr_format';
1458
1459 CREATE TABLE metabib.record_attr (
1460        id              BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
1461        attrs   HSTORE  NOT NULL DEFAULT ''::HSTORE
1462 );
1463 CREATE INDEX metabib_svf_attrs_idx ON metabib.record_attr USING GIST (attrs);
1464 CREATE INDEX metabib_svf_date1_idx ON metabib.record_attr ( (attrs->'date1') );
1465 CREATE INDEX metabib_svf_dates_idx ON metabib.record_attr ( (attrs->'date1'), (attrs->'date2') );
1466
1467 INSERT INTO metabib.record_attr (id,attrs)
1468     SELECT mrd.record, hstore(mrd) - '{id,record}'::TEXT[] FROM metabib.rec_descriptor mrd;
1469
1470 -- Back-compat view ... we're moving to an HSTORE world
1471 CREATE TYPE metabib.rec_desc_type AS (
1472     item_type       TEXT,
1473     item_form       TEXT,
1474     bib_level       TEXT,
1475     control_type    TEXT,
1476     char_encoding   TEXT,
1477     enc_level       TEXT,
1478     audience        TEXT,
1479     lit_form        TEXT,
1480     type_mat        TEXT,
1481     cat_form        TEXT,
1482     pub_status      TEXT,
1483     item_lang       TEXT,
1484     vr_format       TEXT,
1485     date1           TEXT,
1486     date2           TEXT
1487 );
1488
1489 DROP TABLE metabib.rec_descriptor CASCADE;
1490
1491 CREATE VIEW metabib.rec_descriptor AS
1492     SELECT  id,
1493             id AS record,
1494             (populate_record(NULL::metabib.rec_desc_type, attrs)).*
1495       FROM  metabib.record_attr;
1496
1497 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
1498 DECLARE
1499     ldr         TEXT;
1500     tval        TEXT;
1501     tval_rec    RECORD;
1502     bval        TEXT;
1503     bval_rec    RECORD;
1504     retval      config.marc21_rec_type_map%ROWTYPE;
1505 BEGIN
1506     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
1507
1508     IF ldr IS NULL OR ldr = '' THEN
1509         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1510         RETURN retval;
1511     END IF;
1512
1513     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
1514     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
1515
1516
1517     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1518     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1519
1520     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1521
1522     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1523
1524
1525     IF retval.code IS NULL THEN
1526         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1527     END IF;
1528
1529     RETURN retval;
1530 END;
1531 $func$ LANGUAGE PLPGSQL;
1532
1533 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
1534     SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1535 $func$ LANGUAGE SQL;
1536
1537 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
1538 DECLARE
1539     rtype       TEXT;
1540     ff_pos      RECORD;
1541     tag_data    RECORD;
1542     val         TEXT;
1543 BEGIN
1544     rtype := (vandelay.marc21_record_type( marc )).code;
1545     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
1546         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1547             val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1548             RETURN val;
1549         END LOOP;
1550         val := REPEAT( ff_pos.default_val, ff_pos.length );
1551         RETURN val;
1552     END LOOP;
1553
1554     RETURN NULL;
1555 END;
1556 $func$ LANGUAGE PLPGSQL;
1557
1558 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
1559     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
1560 $func$ LANGUAGE SQL;
1561
1562 CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
1563 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
1564 DECLARE
1565     tag_data    TEXT;
1566     rtype       TEXT;
1567     ff_pos      RECORD;
1568     output      biblio.record_ff_map%ROWTYPE;
1569 BEGIN
1570     rtype := (vandelay.marc21_record_type( marc )).code;
1571
1572     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
1573         output.ff_name  := ff_pos.fixed_field;
1574         output.ff_value := NULL;
1575
1576         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(tag) || '"]/text()', marc ) ) x(value) LOOP
1577             output.ff_value := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1578             IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
1579             RETURN NEXT output;
1580             output.ff_value := NULL;
1581         END LOOP;
1582
1583     END LOOP;
1584
1585     RETURN;
1586 END;
1587 $func$ LANGUAGE PLPGSQL;
1588
1589 CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
1590     SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1591 $func$ LANGUAGE SQL;
1592
1593 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1594 DECLARE
1595     rowid   INT := 0;
1596     _007    TEXT;
1597     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
1598     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
1599     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
1600     retval  biblio.marc21_physical_characteristics%ROWTYPE;
1601 BEGIN
1602
1603     _007 := oils_xpath_string( '//*[@tag="007"]', marc );
1604
1605     IF _007 IS NOT NULL AND _007 <> '' THEN
1606         SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
1607
1608         IF ptype.ptype_key IS NOT NULL THEN
1609             FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
1610                 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 );
1611
1612                 IF pval.id IS NOT NULL THEN
1613                     rowid := rowid + 1;
1614                     retval.id := rowid;
1615                     retval.ptype := ptype.ptype_key;
1616                     retval.subfield := psf.id;
1617                     retval.value := pval.id;
1618                     RETURN NEXT retval;
1619                 END IF;
1620
1621             END LOOP;
1622         END IF;
1623     END IF;
1624
1625     RETURN;
1626 END;
1627 $func$ LANGUAGE PLPGSQL;
1628
1629 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1630     SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1631 $func$ LANGUAGE SQL;
1632
1633 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
1634 DECLARE
1635     transformed_xml TEXT;
1636     prev_xfrm       TEXT;
1637     normalizer      RECORD;
1638     xfrm            config.xml_transform%ROWTYPE;
1639     attr_value      TEXT;
1640     new_attrs       HSTORE := ''::HSTORE;
1641     attr_def        config.record_attr_definition%ROWTYPE;
1642 BEGIN
1643
1644     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
1645         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
1646         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
1647         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
1648         RETURN NEW; -- and we're done
1649     END IF;
1650
1651     IF TG_OP = 'UPDATE' THEN -- re-ingest?
1652         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
1653
1654         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
1655             RETURN NEW;
1656         END IF;
1657     END IF;
1658
1659     -- Record authority linking
1660     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
1661     IF NOT FOUND THEN
1662         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
1663     END IF;
1664
1665     -- Flatten and insert the mfr data
1666     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
1667     IF NOT FOUND THEN
1668         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
1669
1670         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
1671         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
1672         IF NOT FOUND THEN
1673             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
1674
1675                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1676                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
1677                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
1678                       WHERE record = NEW.id
1679                             AND tag LIKE attr_def.tag
1680                             AND CASE
1681                                 WHEN attr_def.sf_list IS NOT NULL
1682                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
1683                                 ELSE TRUE
1684                                 END
1685                       GROUP BY tag
1686                       ORDER BY tag
1687                       LIMIT 1;
1688
1689                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1690                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
1691
1692                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1693
1694                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1695
1696                     -- See if we can skip the XSLT ... it's expensive
1697                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1698                         -- Can't skip the transform
1699                         IF xfrm.xslt <> '---' THEN
1700                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
1701                         ELSE
1702                             transformed_xml := NEW.marc;
1703                         END IF;
1704
1705                         prev_xfrm := xfrm.name;
1706                     END IF;
1707
1708                     IF xfrm.name IS NULL THEN
1709                         -- just grab the marcxml (empty) transform
1710                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1711                         prev_xfrm := xfrm.name;
1712                     END IF;
1713
1714                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
1715
1716                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1717                     SELECT  value::TEXT INTO attr_value
1718                       FROM  biblio.marc21_physical_characteristics(NEW.id)
1719                       WHERE subfield = attr_def.phys_char_sf
1720                       LIMIT 1; -- Just in case ...
1721
1722                 END IF;
1723
1724                 -- apply index normalizers to attr_value
1725                 FOR normalizer IN
1726                     SELECT  n.func AS func,
1727                             n.param_count AS param_count,
1728                             m.params AS params
1729                       FROM  config.index_normalizer n
1730                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1731                       WHERE attr = attr_def.name
1732                       ORDER BY m.pos LOOP
1733                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
1734                             quote_literal( attr_value ) ||
1735                             CASE
1736                                 WHEN normalizer.param_count > 0
1737                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1738                                     ELSE ''
1739                                 END ||
1740                             ')' INTO attr_value;
1741
1742                 END LOOP;
1743
1744                 -- Add the new value to the hstore
1745                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
1746
1747             END LOOP;
1748
1749             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
1750                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
1751             ELSE
1752                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
1753             END IF;
1754
1755         END IF;
1756     END IF;
1757
1758     -- Gather and insert the field entry data
1759     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
1760
1761     -- Located URI magic
1762     IF TG_OP = 'INSERT' THEN
1763         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1764         IF NOT FOUND THEN
1765             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1766         END IF;
1767     ELSE
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     END IF;
1773
1774     -- (re)map metarecord-bib linking
1775     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
1776         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
1777         IF NOT FOUND THEN
1778             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1779         END IF;
1780     ELSE -- we're doing an update, and we're not deleted, remap
1781         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
1782         IF NOT FOUND THEN
1783             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1784         END IF;
1785     END IF;
1786
1787     RETURN NEW;
1788 END;
1789 $func$ LANGUAGE PLPGSQL;
1790
1791 DROP FUNCTION metabib.reingest_metabib_rec_descriptor( bib_id BIGINT );
1792
1793 CREATE OR REPLACE FUNCTION public.approximate_date( TEXT, TEXT ) RETURNS TEXT AS $func$
1794         SELECT REGEXP_REPLACE( $1, E'\\D', $2, 'g' );
1795 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1796
1797 CREATE OR REPLACE FUNCTION public.approximate_low_date( TEXT ) RETURNS TEXT AS $func$
1798         SELECT approximate_date( $1, '0');
1799 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1800
1801 CREATE OR REPLACE FUNCTION public.approximate_high_date( TEXT ) RETURNS TEXT AS $func$
1802         SELECT approximate_date( $1, '9');
1803 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1804
1805 CREATE OR REPLACE FUNCTION public.integer_or_null( TEXT ) RETURNS TEXT AS $func$
1806         SELECT CASE WHEN $1 ~ E'^\\d+$' THEN $1 ELSE NULL END
1807 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1808
1809 CREATE OR REPLACE FUNCTION public.content_or_null( TEXT ) RETURNS TEXT AS $func$
1810         SELECT CASE WHEN $1 ~ E'^\\s*$' THEN NULL ELSE $1 END
1811 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1812
1813 CREATE OR REPLACE FUNCTION public.force_to_isbn13( TEXT ) RETURNS TEXT AS $func$
1814     use Business::ISBN;
1815     use strict;
1816     use warnings;
1817
1818     # Find the first ISBN, force it to ISBN13 and return it
1819
1820     my $input = shift;
1821
1822     foreach my $word (split(/\s/, $input)) {
1823         my $isbn = Business::ISBN->new($word);
1824
1825         # First check the checksum; if it is not valid, fix it and add the original
1826         # bad-checksum ISBN to the output
1827         if ($isbn && $isbn->is_valid_checksum() == Business::ISBN::BAD_CHECKSUM) {
1828             $isbn->fix_checksum();
1829         }
1830
1831         # If we now have a valid ISBN, force it to ISBN13 and return it
1832         return $isbn->as_isbn13->isbn if ($isbn && $isbn->is_valid());
1833     }
1834     return undef;
1835 $func$ LANGUAGE PLPERLU;
1836
1837 COMMENT ON FUNCTION public.force_to_isbn13(TEXT) IS $$
1838 /*
1839  * Copyright (C) 2011 Equinox Software
1840  * Mike Rylander <mrylander@gmail.com>
1841  *
1842  * Inspired by translate_isbn1013
1843  *
1844  * The force_to_isbn13 function takes an input ISBN and returns the ISBN13
1845  * version without hypens and with a repaired checksum if the checksum was bad
1846  */
1847 $$;
1848
1849 -- 0496
1850 UPDATE config.metabib_field
1851     SET xpath = $$//marc:datafield[@tag='024' and @ind1='1']/marc:subfield[@code='a' or @code='z']$$
1852     WHERE field_class = 'identifier' AND name = 'upc';
1853
1854 UPDATE config.metabib_field
1855     SET xpath = $$//marc:datafield[@tag='024' and @ind1='2']/marc:subfield[@code='a' or @code='z']$$
1856     WHERE field_class = 'identifier' AND name = 'ismn';
1857
1858 UPDATE config.metabib_field
1859     SET xpath = $$//marc:datafield[@tag='024' and @ind1='3']/marc:subfield[@code='a' or @code='z']$$
1860     WHERE field_class = 'identifier' AND name = 'ean';
1861
1862 UPDATE config.metabib_field
1863     SET xpath = $$//marc:datafield[@tag='024' and @ind1='0']/marc:subfield[@code='a' or @code='z']$$
1864     WHERE field_class = 'identifier' AND name = 'isrc';
1865
1866 UPDATE config.metabib_field
1867     SET xpath = $$//marc:datafield[@tag='024' and @ind1='4']/marc:subfield[@code='a' or @code='z']$$
1868     WHERE field_class = 'identifier' AND name = 'sici';
1869
1870 -- 0497
1871 INSERT into config.org_unit_setting_type
1872 ( name, label, description, datatype ) VALUES
1873
1874 ( 'ui.patron.edit.au.active.show',
1875     oils_i18n_gettext('ui.patron.edit.au.active.show', 'GUI: Show active field on patron registration', 'coust', 'label'),
1876     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'),
1877     'bool'),
1878 ( 'ui.patron.edit.au.active.suggest',
1879     oils_i18n_gettext('ui.patron.edit.au.active.suggest', 'GUI: Suggest active field on patron registration', 'coust', 'label'),
1880     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'),
1881     'bool'),
1882 ( 'ui.patron.edit.au.alert_message.show',
1883     oils_i18n_gettext('ui.patron.edit.au.alert_message.show', 'GUI: Show alert_message field on patron registration', 'coust', 'label'),
1884     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'),
1885     'bool'),
1886 ( 'ui.patron.edit.au.alert_message.suggest',
1887     oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest', 'GUI: Suggest alert_message field on patron registration', 'coust', 'label'),
1888     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'),
1889     'bool'),
1890 ( 'ui.patron.edit.au.alias.show',
1891     oils_i18n_gettext('ui.patron.edit.au.alias.show', 'GUI: Show alias field on patron registration', 'coust', 'label'),
1892     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'),
1893     'bool'),
1894 ( 'ui.patron.edit.au.alias.suggest',
1895     oils_i18n_gettext('ui.patron.edit.au.alias.suggest', 'GUI: Suggest alias field on patron registration', 'coust', 'label'),
1896     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'),
1897     'bool'),
1898 ( 'ui.patron.edit.au.barred.show',
1899     oils_i18n_gettext('ui.patron.edit.au.barred.show', 'GUI: Show barred field on patron registration', 'coust', 'label'),
1900     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'),
1901     'bool'),
1902 ( 'ui.patron.edit.au.barred.suggest',
1903     oils_i18n_gettext('ui.patron.edit.au.barred.suggest', 'GUI: Suggest barred field on patron registration', 'coust', 'label'),
1904     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'),
1905     'bool'),
1906 ( 'ui.patron.edit.au.claims_never_checked_out_count.show',
1907     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'),
1908     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'),
1909     'bool'),
1910 ( 'ui.patron.edit.au.claims_never_checked_out_count.suggest',
1911     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'),
1912     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'),
1913     'bool'),
1914 ( 'ui.patron.edit.au.claims_returned_count.show',
1915     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.show', 'GUI: Show claims_returned_count field on patron registration', 'coust', 'label'),
1916     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'),
1917     'bool'),
1918 ( 'ui.patron.edit.au.claims_returned_count.suggest',
1919     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.suggest', 'GUI: Suggest claims_returned_count field on patron registration', 'coust', 'label'),
1920     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'),
1921     'bool'),
1922 ( 'ui.patron.edit.au.day_phone.example',
1923     oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'GUI: Example for day_phone field on patron registration', 'coust', 'label'),
1924     oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'The Example for validation on the day_phone field in patron registration.', 'coust', 'description'),
1925     'string'),
1926 ( 'ui.patron.edit.au.day_phone.regex',
1927     oils_i18n_gettext('ui.patron.edit.au.day_phone.regex', 'GUI: Regex for day_phone field on patron registration', 'coust', 'label'),
1928     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'),
1929     'string'),
1930 ( 'ui.patron.edit.au.day_phone.require',
1931     oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'GUI: Require day_phone field on patron registration', 'coust', 'label'),
1932     oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'The day_phone field will be required on the patron registration screen.', 'coust', 'description'),
1933     'bool'),
1934 ( 'ui.patron.edit.au.day_phone.show',
1935     oils_i18n_gettext('ui.patron.edit.au.day_phone.show', 'GUI: Show day_phone field on patron registration', 'coust', 'label'),
1936     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'),
1937     'bool'),
1938 ( 'ui.patron.edit.au.day_phone.suggest',
1939     oils_i18n_gettext('ui.patron.edit.au.day_phone.suggest', 'GUI: Suggest day_phone field on patron registration', 'coust', 'label'),
1940     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'),
1941     'bool'),
1942 ( 'ui.patron.edit.au.dob.calendar',
1943     oils_i18n_gettext('ui.patron.edit.au.dob.calendar', 'GUI: Show calendar widget for dob field on patron registration', 'coust', 'label'),
1944     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'),
1945     'bool'),
1946 ( 'ui.patron.edit.au.dob.require',
1947     oils_i18n_gettext('ui.patron.edit.au.dob.require', 'GUI: Require dob field on patron registration', 'coust', 'label'),
1948     oils_i18n_gettext('ui.patron.edit.au.dob.require', 'The dob field will be required on the patron registration screen.', 'coust', 'description'),
1949     'bool'),
1950 ( 'ui.patron.edit.au.dob.show',
1951     oils_i18n_gettext('ui.patron.edit.au.dob.show', 'GUI: Show dob field on patron registration', 'coust', 'label'),
1952     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'),
1953     'bool'),
1954 ( 'ui.patron.edit.au.dob.suggest',
1955     oils_i18n_gettext('ui.patron.edit.au.dob.suggest', 'GUI: Suggest dob field on patron registration', 'coust', 'label'),
1956     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'),
1957     'bool'),
1958 ( 'ui.patron.edit.au.email.example',
1959     oils_i18n_gettext('ui.patron.edit.au.email.example', 'GUI: Example for email field on patron registration', 'coust', 'label'),
1960     oils_i18n_gettext('ui.patron.edit.au.email.example', 'The Example for validation on the email field in patron registration.', 'coust', 'description'),
1961     'string'),
1962 ( 'ui.patron.edit.au.email.regex',
1963     oils_i18n_gettext('ui.patron.edit.au.email.regex', 'GUI: Regex for email field on patron registration', 'coust', 'label'),
1964     oils_i18n_gettext('ui.patron.edit.au.email.regex', 'The Regular Expression for validation on the email field in patron registration.', 'coust', 'description'),
1965     'string'),
1966 ( 'ui.patron.edit.au.email.require',
1967     oils_i18n_gettext('ui.patron.edit.au.email.require', 'GUI: Require email field on patron registration', 'coust', 'label'),
1968     oils_i18n_gettext('ui.patron.edit.au.email.require', 'The email field will be required on the patron registration screen.', 'coust', 'description'),
1969     'bool'),
1970 ( 'ui.patron.edit.au.email.show',
1971     oils_i18n_gettext('ui.patron.edit.au.email.show', 'GUI: Show email field on patron registration', 'coust', 'label'),
1972     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'),
1973     'bool'),
1974 ( 'ui.patron.edit.au.email.suggest',
1975     oils_i18n_gettext('ui.patron.edit.au.email.suggest', 'GUI: Suggest email field on patron registration', 'coust', 'label'),
1976     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'),
1977     'bool'),
1978 ( 'ui.patron.edit.au.evening_phone.example',
1979     oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'GUI: Example for evening_phone field on patron registration', 'coust', 'label'),
1980     oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'The Example for validation on the evening_phone field in patron registration.', 'coust', 'description'),
1981     'string'),
1982 ( 'ui.patron.edit.au.evening_phone.regex',
1983     oils_i18n_gettext('ui.patron.edit.au.evening_phone.regex', 'GUI: Regex for evening_phone field on patron registration', 'coust', 'label'),
1984     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'),
1985     'string'),
1986 ( 'ui.patron.edit.au.evening_phone.require',
1987     oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'GUI: Require evening_phone field on patron registration', 'coust', 'label'),
1988     oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'The evening_phone field will be required on the patron registration screen.', 'coust', 'description'),
1989     'bool'),
1990 ( 'ui.patron.edit.au.evening_phone.show',
1991     oils_i18n_gettext('ui.patron.edit.au.evening_phone.show', 'GUI: Show evening_phone field on patron registration', 'coust', 'label'),
1992     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'),
1993     'bool'),
1994 ( 'ui.patron.edit.au.evening_phone.suggest',
1995     oils_i18n_gettext('ui.patron.edit.au.evening_phone.suggest', 'GUI: Suggest evening_phone field on patron registration', 'coust', 'label'),
1996     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'),
1997     'bool'),
1998 ( 'ui.patron.edit.au.ident_value.show',
1999     oils_i18n_gettext('ui.patron.edit.au.ident_value.show', 'GUI: Show ident_value field on patron registration', 'coust', 'label'),
2000     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'),
2001     'bool'),
2002 ( 'ui.patron.edit.au.ident_value.suggest',
2003     oils_i18n_gettext('ui.patron.edit.au.ident_value.suggest', 'GUI: Suggest ident_value field on patron registration', 'coust', 'label'),
2004     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'),
2005     'bool'),
2006 ( 'ui.patron.edit.au.ident_value2.show',
2007     oils_i18n_gettext('ui.patron.edit.au.ident_value2.show', 'GUI: Show ident_value2 field on patron registration', 'coust', 'label'),
2008     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'),
2009     'bool'),
2010 ( 'ui.patron.edit.au.ident_value2.suggest',
2011     oils_i18n_gettext('ui.patron.edit.au.ident_value2.suggest', 'GUI: Suggest ident_value2 field on patron registration', 'coust', 'label'),
2012     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'),
2013     'bool'),
2014 ( 'ui.patron.edit.au.juvenile.show',
2015     oils_i18n_gettext('ui.patron.edit.au.juvenile.show', 'GUI: Show juvenile field on patron registration', 'coust', 'label'),
2016     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'),
2017     'bool'),
2018 ( 'ui.patron.edit.au.juvenile.suggest',
2019     oils_i18n_gettext('ui.patron.edit.au.juvenile.suggest', 'GUI: Suggest juvenile field on patron registration', 'coust', 'label'),
2020     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'),
2021     'bool'),
2022 ( 'ui.patron.edit.au.master_account.show',
2023     oils_i18n_gettext('ui.patron.edit.au.master_account.show', 'GUI: Show master_account field on patron registration', 'coust', 'label'),
2024     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'),
2025     'bool'),
2026 ( 'ui.patron.edit.au.master_account.suggest',
2027     oils_i18n_gettext('ui.patron.edit.au.master_account.suggest', 'GUI: Suggest master_account field on patron registration', 'coust', 'label'),
2028     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'),
2029     'bool'),
2030 ( 'ui.patron.edit.au.other_phone.example',
2031     oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'GUI: Example for other_phone field on patron registration', 'coust', 'label'),
2032     oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'The Example for validation on the other_phone field in patron registration.', 'coust', 'description'),
2033     'string'),
2034 ( 'ui.patron.edit.au.other_phone.regex',
2035     oils_i18n_gettext('ui.patron.edit.au.other_phone.regex', 'GUI: Regex for other_phone field on patron registration', 'coust', 'label'),
2036     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'),
2037     'string'),
2038 ( 'ui.patron.edit.au.other_phone.require',
2039     oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'GUI: Require other_phone field on patron registration', 'coust', 'label'),
2040     oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'The other_phone field will be required on the patron registration screen.', 'coust', 'description'),
2041     'bool'),
2042 ( 'ui.patron.edit.au.other_phone.show',
2043     oils_i18n_gettext('ui.patron.edit.au.other_phone.show', 'GUI: Show other_phone field on patron registration', 'coust', 'label'),
2044     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'),
2045     'bool'),
2046 ( 'ui.patron.edit.au.other_phone.suggest',
2047     oils_i18n_gettext('ui.patron.edit.au.other_phone.suggest', 'GUI: Suggest other_phone field on patron registration', 'coust', 'label'),
2048     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'),
2049     'bool'),
2050 ( 'ui.patron.edit.au.second_given_name.show',
2051     oils_i18n_gettext('ui.patron.edit.au.second_given_name.show', 'GUI: Show second_given_name field on patron registration', 'coust', 'label'),
2052     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'),
2053     'bool'),
2054 ( 'ui.patron.edit.au.second_given_name.suggest',
2055     oils_i18n_gettext('ui.patron.edit.au.second_given_name.suggest', 'GUI: Suggest second_given_name field on patron registration', 'coust', 'label'),
2056     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'),
2057     'bool'),
2058 ( 'ui.patron.edit.au.suffix.show',
2059     oils_i18n_gettext('ui.patron.edit.au.suffix.show', 'GUI: Show suffix field on patron registration', 'coust', 'label'),
2060     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'),
2061     'bool'),
2062 ( 'ui.patron.edit.au.suffix.suggest',
2063     oils_i18n_gettext('ui.patron.edit.au.suffix.suggest', 'GUI: Suggest suffix field on patron registration', 'coust', 'label'),
2064     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'),
2065     'bool'),
2066 ( 'ui.patron.edit.aua.county.require',
2067     oils_i18n_gettext('ui.patron.edit.aua.county.require', 'GUI: Require county field on patron registration', 'coust', 'label'),
2068     oils_i18n_gettext('ui.patron.edit.aua.county.require', 'The county field will be required on the patron registration screen.', 'coust', 'description'),
2069     'bool'),
2070 ( 'ui.patron.edit.aua.post_code.example',
2071     oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'GUI: Example for post_code field on patron registration', 'coust', 'label'),
2072     oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'The Example for validation on the post_code field in patron registration.', 'coust', 'description'),
2073     'string'),
2074 ( 'ui.patron.edit.aua.post_code.regex',
2075     oils_i18n_gettext('ui.patron.edit.aua.post_code.regex', 'GUI: Regex for post_code field on patron registration', 'coust', 'label'),
2076     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'),
2077     'string'),
2078 ( 'ui.patron.edit.default_suggested',
2079     oils_i18n_gettext('ui.patron.edit.default_suggested', 'GUI: Default showing suggested patron registration fields', 'coust', 'label'),
2080     oils_i18n_gettext('ui.patron.edit.default_suggested', 'Instead of All fields, show just suggested fields in patron registration by default.', 'coust', 'description'),
2081     'bool'),
2082 ( 'ui.patron.edit.phone.example',
2083     oils_i18n_gettext('ui.patron.edit.phone.example', 'GUI: Example for phone fields on patron registration', 'coust', 'label'),
2084     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'),
2085     'string'),
2086 ( 'ui.patron.edit.phone.regex',
2087     oils_i18n_gettext('ui.patron.edit.phone.regex', 'GUI: Regex for phone fields on patron registration', 'coust', 'label'),
2088     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'),
2089     'string');
2090
2091 -- update actor.usr_address indexes
2092 DROP INDEX IF EXISTS actor.actor_usr_addr_street1_idx;
2093 DROP INDEX IF EXISTS actor.actor_usr_addr_street2_idx;
2094 DROP INDEX IF EXISTS actor.actor_usr_addr_city_idx;
2095 DROP INDEX IF EXISTS actor.actor_usr_addr_state_idx; 
2096 DROP INDEX IF EXISTS actor.actor_usr_addr_post_code_idx;
2097
2098 CREATE INDEX actor_usr_addr_street1_idx ON actor.usr_address (evergreen.lowercase(street1));
2099 CREATE INDEX actor_usr_addr_street2_idx ON actor.usr_address (evergreen.lowercase(street2));
2100 CREATE INDEX actor_usr_addr_city_idx ON actor.usr_address (evergreen.lowercase(city));
2101 CREATE INDEX actor_usr_addr_state_idx ON actor.usr_address (evergreen.lowercase(state));
2102 CREATE INDEX actor_usr_addr_post_code_idx ON actor.usr_address (evergreen.lowercase(post_code));
2103
2104 -- update actor.usr indexes
2105 DROP INDEX IF EXISTS actor.actor_usr_first_given_name_idx;
2106 DROP INDEX IF EXISTS actor.actor_usr_second_given_name_idx;
2107 DROP INDEX IF EXISTS actor.actor_usr_family_name_idx;
2108 DROP INDEX IF EXISTS actor.actor_usr_email_idx;
2109 DROP INDEX IF EXISTS actor.actor_usr_day_phone_idx;
2110 DROP INDEX IF EXISTS actor.actor_usr_evening_phone_idx;
2111 DROP INDEX IF EXISTS actor.actor_usr_other_phone_idx;
2112 DROP INDEX IF EXISTS actor.actor_usr_ident_value_idx;
2113 DROP INDEX IF EXISTS actor.actor_usr_ident_value2_idx;
2114
2115 CREATE INDEX actor_usr_first_given_name_idx ON actor.usr (evergreen.lowercase(first_given_name));
2116 CREATE INDEX actor_usr_second_given_name_idx ON actor.usr (evergreen.lowercase(second_given_name));
2117 CREATE INDEX actor_usr_family_name_idx ON actor.usr (evergreen.lowercase(family_name));
2118 CREATE INDEX actor_usr_email_idx ON actor.usr (evergreen.lowercase(email));
2119 CREATE INDEX actor_usr_day_phone_idx ON actor.usr (evergreen.lowercase(day_phone));
2120 CREATE INDEX actor_usr_evening_phone_idx ON actor.usr (evergreen.lowercase(evening_phone));
2121 CREATE INDEX actor_usr_other_phone_idx ON actor.usr (evergreen.lowercase(other_phone));
2122 CREATE INDEX actor_usr_ident_value_idx ON actor.usr (evergreen.lowercase(ident_value));
2123 CREATE INDEX actor_usr_ident_value2_idx ON actor.usr (evergreen.lowercase(ident_value2));
2124
2125 -- update actor.card indexes
2126 DROP INDEX IF EXISTS actor.actor_card_barcode_evergreen_lowercase_idx;
2127 CREATE INDEX actor_card_barcode_evergreen_lowercase_idx ON actor.card (evergreen.lowercase(barcode));
2128
2129 CREATE OR REPLACE FUNCTION vandelay.match_bib_record ( ) RETURNS TRIGGER AS $func$
2130 DECLARE
2131     attr        RECORD;
2132     attr_def    RECORD;
2133     eg_rec      RECORD;
2134     id_value    TEXT;
2135     exact_id    BIGINT;
2136 BEGIN
2137
2138     DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
2139
2140     SELECT * INTO attr_def FROM vandelay.bib_attr_definition WHERE xpath = '//*[@tag="901"]/*[@code="c"]' ORDER BY id LIMIT 1;
2141
2142     IF attr_def IS NOT NULL AND attr_def.id IS NOT NULL THEN
2143         id_value := extract_marc_field('vandelay.queued_bib_record', NEW.id, attr_def.xpath, attr_def.remove);
2144     
2145         IF id_value IS NOT NULL AND id_value <> '' AND id_value ~ $r$^\d+$$r$ THEN
2146             SELECT id INTO exact_id FROM biblio.record_entry WHERE id = id_value::BIGINT AND NOT deleted;
2147             SELECT * INTO attr FROM vandelay.queued_bib_record_attr WHERE record = NEW.id and field = attr_def.id LIMIT 1;
2148             IF exact_id IS NOT NULL THEN
2149                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, exact_id);
2150             END IF;
2151         END IF;
2152     END IF;
2153
2154     IF exact_id IS NULL THEN
2155         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
2156     
2157                 -- All numbers? check for an id match
2158                 IF (attr.attr_value ~ $r$^\d+$$r$) THEN
2159                 FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE id = attr.attr_value::BIGINT AND deleted IS FALSE LOOP
2160                         INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
2161                         END LOOP;
2162                 END IF;
2163     
2164                 -- Looks like an ISBN? check for an isbn match
2165                 IF (attr.attr_value ~* $r$^[0-9x]+$$r$ AND character_length(attr.attr_value) IN (10,13)) THEN
2166                 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
2167                                 PERFORM id FROM biblio.record_entry WHERE id = eg_rec.record AND deleted IS FALSE;
2168                                 IF FOUND THEN
2169                                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('isbn', attr.id, NEW.id, eg_rec.record);
2170                                 END IF;
2171                         END LOOP;
2172     
2173                         -- subcheck for isbn-as-tcn
2174                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = 'i' || attr.attr_value AND deleted IS FALSE LOOP
2175                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2176                 END LOOP;
2177                 END IF;
2178     
2179                 -- check for an OCLC tcn_value match
2180                 IF (attr.attr_value ~ $r$^o\d+$$r$) THEN
2181                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = regexp_replace(attr.attr_value,'^o','ocm') AND deleted IS FALSE LOOP
2182                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2183                 END LOOP;
2184                 END IF;
2185     
2186                 -- check for a direct tcn_value match
2187             FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = attr.attr_value AND deleted IS FALSE LOOP
2188                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2189             END LOOP;
2190     
2191                 -- check for a direct item barcode match
2192             FOR eg_rec IN
2193                     SELECT  DISTINCT b.*
2194                       FROM  biblio.record_entry b
2195                             JOIN asset.call_number cn ON (cn.record = b.id)
2196                             JOIN asset.copy cp ON (cp.call_number = cn.id)
2197                       WHERE cp.barcode = attr.attr_value AND cp.deleted IS FALSE
2198             LOOP
2199                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
2200             END LOOP;
2201     
2202         END LOOP;
2203     END IF;
2204
2205     RETURN NULL;
2206 END;
2207 $func$ LANGUAGE PLPGSQL;
2208
2209
2210 -- 0499
2211 CREATE OR REPLACE FUNCTION asset.label_normalizer_generic(TEXT) RETURNS TEXT AS $func$
2212     # Created after looking at the Koha C4::ClassSortRoutine::Generic module,
2213     # thus could probably be considered a derived work, although nothing was
2214     # directly copied - but to err on the safe side of providing attribution:
2215     # Copyright (C) 2007 LibLime
2216     # Copyright (C) 2011 Equinox Software, Inc (Steve Callendar)
2217     # Licensed under the GPL v2 or later
2218
2219     use strict;
2220     use warnings;
2221
2222     # Converts the callnumber to uppercase
2223     # Strips spaces from start and end of the call number
2224     # Converts anything other than letters, digits, and periods into spaces
2225     # Collapses multiple spaces into a single underscore
2226     my $callnum = uc(shift);
2227     $callnum =~ s/^\s//g;
2228     $callnum =~ s/\s$//g;
2229     # NOTE: this previously used underscores, but this caused sorting issues
2230     # for the "before" half of page 0 on CN browse, sorting CNs containing a
2231     # decimal before "whole number" CNs
2232     $callnum =~ s/[^A-Z0-9_.]/ /g;
2233     $callnum =~ s/ {2,}/ /g;
2234
2235     return $callnum;
2236 $func$ LANGUAGE PLPERLU;
2237
2238
2239
2240 -- 0501
2241 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('language','Language (2.0 compat version)','Lang');
2242 UPDATE metabib.record_attr SET attrs = attrs || hstore('language',(attrs->'item_lang'));
2243
2244 -- 0502
2245 -- Dewey fields
2246 UPDATE asset.call_number_class
2247     SET field = '080ab,082ab,092abef'
2248     WHERE id = 2
2249 ;
2250
2251 -- LC fields
2252 UPDATE asset.call_number_class
2253     SET field = '050ab,055ab,090abef'
2254     WHERE id = 3
2255 ;
2256
2257 -- FAIR WARNING:
2258 -- Using a tool such as pgadmin to run this script may fail
2259 -- If it does, try psql command line.
2260
2261 -- Change this to FALSE to disable updating existing circs
2262 -- Otherwise will use the fine interval for the grace period
2263 \set CircGrace TRUE
2264
2265 -- 0503
2266 -- New Columns
2267
2268 ALTER TABLE config.circ_matrix_matchpoint
2269     ADD COLUMN grace_period INTERVAL;
2270
2271 ALTER TABLE config.rule_recurring_fine
2272     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '1 day';
2273
2274 ALTER TABLE action.circulation
2275     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
2276
2277 ALTER TABLE action.aged_circulation
2278     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
2279
2280 -- Remove defaults needed to stop null complaints
2281
2282 ALTER TABLE action.circulation
2283     ALTER COLUMN grace_period DROP DEFAULT;
2284
2285 ALTER TABLE action.aged_circulation
2286     ALTER COLUMN grace_period DROP DEFAULT;
2287
2288 -- Drop Views
2289
2290 DROP VIEW action.all_circulation;
2291 DROP VIEW action.open_circulation;
2292 DROP VIEW action.billable_circulations;
2293
2294 -- Replace Views
2295
2296 CREATE OR REPLACE VIEW action.all_circulation AS
2297     SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
2298         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
2299         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
2300         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
2301         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
2302         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
2303       FROM  action.aged_circulation
2304             UNION ALL
2305     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,
2306         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,
2307         cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
2308         circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
2309         circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
2310         circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
2311         circ.parent_circ
2312       FROM  action.circulation circ
2313         JOIN asset.copy cp ON (circ.target_copy = cp.id)
2314         JOIN asset.call_number cn ON (cp.call_number = cn.id)
2315         JOIN actor.usr p ON (circ.usr = p.id)
2316         LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
2317         LEFT JOIN actor.usr_address b ON (p.billing_address = a.id);
2318
2319 CREATE OR REPLACE VIEW action.open_circulation AS
2320         SELECT  *
2321           FROM  action.circulation
2322           WHERE checkin_time IS NULL
2323           ORDER BY due_date;
2324                 
2325
2326 CREATE OR REPLACE VIEW action.billable_circulations AS
2327         SELECT  *
2328           FROM  action.circulation
2329           WHERE xact_finish IS NULL;
2330
2331 -- Drop Functions that rely on types
2332
2333 DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT, BOOL);
2334 DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT);
2335 DROP FUNCTION action.item_user_renew_test(INT, BIGINT, INT);
2336
2337 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$
2338 DECLARE
2339     user_object             actor.usr%ROWTYPE;
2340     standing_penalty        config.standing_penalty%ROWTYPE;
2341     item_object             asset.copy%ROWTYPE;
2342     item_status_object      config.copy_status%ROWTYPE;
2343     item_location_object    asset.copy_location%ROWTYPE;
2344     result                  action.circ_matrix_test_result;
2345     circ_test               action.found_circ_matrix_matchpoint;
2346     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
2347     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
2348     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
2349     hold_ratio              action.hold_stats%ROWTYPE;
2350     penalty_type            TEXT;
2351     items_out               INT;
2352     context_org_list        INT[];
2353     done                    BOOL := FALSE;
2354 BEGIN
2355     -- Assume success unless we hit a failure condition
2356     result.success := TRUE;
2357
2358     -- Fail if the user is BARRED
2359     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
2360
2361     -- Fail if we couldn't find the user 
2362     IF user_object.id IS NULL THEN
2363         result.fail_part := 'no_user';
2364         result.success := FALSE;
2365         done := TRUE;
2366         RETURN NEXT result;
2367         RETURN;
2368     END IF;
2369
2370     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
2371
2372     -- Fail if we couldn't find the item 
2373     IF item_object.id IS NULL THEN
2374         result.fail_part := 'no_item';
2375         result.success := FALSE;
2376         done := TRUE;
2377         RETURN NEXT result;
2378         RETURN;
2379     END IF;
2380
2381     IF user_object.barred IS TRUE THEN
2382         result.fail_part := 'actor.usr.barred';
2383         result.success := FALSE;
2384         done := TRUE;
2385         RETURN NEXT result;
2386     END IF;
2387
2388     -- Fail if the item can't circulate
2389     IF item_object.circulate IS FALSE THEN
2390         result.fail_part := 'asset.copy.circulate';
2391         result.success := FALSE;
2392         done := TRUE;
2393         RETURN NEXT result;
2394     END IF;
2395
2396     -- Fail if the item isn't in a circulateable status on a non-renewal
2397     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
2398         result.fail_part := 'asset.copy.status';
2399         result.success := FALSE;
2400         done := TRUE;
2401         RETURN NEXT result;
2402     ELSIF renewal AND item_object.status <> 1 THEN
2403         result.fail_part := 'asset.copy.status';
2404         result.success := FALSE;
2405         done := TRUE;
2406         RETURN NEXT result;
2407     END IF;
2408
2409     -- Fail if the item can't circulate because of the shelving location
2410     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
2411     IF item_location_object.circulate IS FALSE THEN
2412         result.fail_part := 'asset.copy_location.circulate';
2413         result.success := FALSE;
2414         done := TRUE;
2415         RETURN NEXT result;
2416     END IF;
2417
2418     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
2419
2420     circ_matchpoint             := circ_test.matchpoint;
2421     result.matchpoint           := circ_matchpoint.id;
2422     result.circulate            := circ_matchpoint.circulate;
2423     result.duration_rule        := circ_matchpoint.duration_rule;
2424     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
2425     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
2426     result.hard_due_date        := circ_matchpoint.hard_due_date;
2427     result.renewals             := circ_matchpoint.renewals;
2428     result.grace_period         := circ_matchpoint.grace_period;
2429     result.buildrows            := circ_test.buildrows;
2430
2431     -- Fail if we couldn't find a matchpoint
2432     IF circ_test.success = false THEN
2433         result.fail_part := 'no_matchpoint';
2434         result.success := FALSE;
2435         done := TRUE;
2436         RETURN NEXT result;
2437         RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
2438     END IF;
2439
2440     -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
2441     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
2442
2443     IF renewal THEN
2444         penalty_type = '%RENEW%';
2445     ELSE
2446         penalty_type = '%CIRC%';
2447     END IF;
2448
2449     FOR standing_penalty IN
2450         SELECT  DISTINCT csp.*
2451           FROM  actor.usr_standing_penalty usp
2452                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
2453           WHERE usr = match_user
2454                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
2455                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
2456                 AND csp.block_list LIKE penalty_type LOOP
2457
2458         result.fail_part := standing_penalty.name;
2459         result.success := FALSE;
2460         done := TRUE;
2461         RETURN NEXT result;
2462     END LOOP;
2463
2464     -- Fail if the test is set to hard non-circulating
2465     IF circ_matchpoint.circulate IS FALSE THEN
2466         result.fail_part := 'config.circ_matrix_test.circulate';
2467         result.success := FALSE;
2468         done := TRUE;
2469         RETURN NEXT result;
2470     END IF;
2471
2472     -- Fail if the total copy-hold ratio is too low
2473     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
2474         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
2475         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
2476             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
2477             result.success := FALSE;
2478             done := TRUE;
2479             RETURN NEXT result;
2480         END IF;
2481     END IF;
2482
2483     -- Fail if the available copy-hold ratio is too low
2484     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
2485         IF hold_ratio.hold_count IS NULL THEN
2486             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
2487         END IF;
2488         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
2489             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
2490             result.success := FALSE;
2491             done := TRUE;
2492             RETURN NEXT result;
2493         END IF;
2494     END IF;
2495
2496     -- Fail if the user has too many items with specific circ_modifiers checked out
2497     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
2498         SELECT  INTO items_out COUNT(*)
2499           FROM  action.circulation circ
2500             JOIN asset.copy cp ON (cp.id = circ.target_copy)
2501           WHERE circ.usr = match_user
2502                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
2503             AND circ.checkin_time IS NULL
2504             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
2505             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);
2506         IF items_out >= out_by_circ_mod.items_out THEN
2507             result.fail_part := 'config.circ_matrix_circ_mod_test';
2508             result.success := FALSE;
2509             done := TRUE;
2510             RETURN NEXT result;
2511         END IF;
2512     END LOOP;
2513
2514     -- If we passed everything, return the successful matchpoint id
2515     IF NOT done THEN
2516         RETURN NEXT result;
2517     END IF;
2518
2519     RETURN;
2520 END;
2521 $func$ LANGUAGE plpgsql;
2522
2523 CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2524     SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
2525 $func$ LANGUAGE SQL;
2526
2527 CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2528     SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
2529 $func$ LANGUAGE SQL;
2530
2531 -- Update recurring fine rules
2532 UPDATE config.rule_recurring_fine SET grace_period=recurrence_interval;
2533
2534 -- Update Circulation Data
2535 -- Only update if we were told to and the circ hasn't been checked in
2536 UPDATE action.circulation SET grace_period=fine_interval WHERE :CircGrace AND (checkin_time IS NULL);
2537
2538 -- 0504
2539 CREATE TABLE biblio.monograph_part (
2540     id              SERIAL  PRIMARY KEY,
2541     record          BIGINT  NOT NULL REFERENCES biblio.record_entry (id),
2542     label           TEXT    NOT NULL,
2543     label_sortkey   TEXT    NOT NULL,
2544     CONSTRAINT record_label_unique UNIQUE (record,label)
2545 );
2546
2547 CREATE OR REPLACE FUNCTION biblio.normalize_biblio_monograph_part_sortkey () RETURNS TRIGGER AS $$
2548 BEGIN
2549     NEW.label_sortkey := REGEXP_REPLACE(
2550         evergreen.lpad_number_substrings(
2551             naco_normalize(NEW.label),
2552             '0',
2553             10
2554         ),
2555         E'\\s+',
2556         '',
2557         'g'
2558     );
2559     RETURN NEW;
2560 END;
2561 $$ LANGUAGE PLPGSQL;
2562
2563 CREATE TRIGGER norm_sort_label BEFORE INSERT OR UPDATE ON biblio.monograph_part FOR EACH ROW EXECUTE PROCEDURE biblio.normalize_biblio_monograph_part_sortkey();
2564
2565 CREATE TABLE asset.copy_part_map (
2566     id          SERIAL  PRIMARY KEY,
2567     target_copy BIGINT  NOT NULL, -- points o asset.copy
2568     part        INT     NOT NULL REFERENCES biblio.monograph_part (id) ON DELETE CASCADE
2569 );
2570 CREATE UNIQUE INDEX copy_part_map_cp_part_idx ON asset.copy_part_map (target_copy, part);
2571
2572 CREATE TABLE asset.call_number_prefix (
2573        id                      SERIAL   PRIMARY KEY,
2574        owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
2575        label               TEXT                NOT NULL, -- i18n
2576        label_sortkey   TEXT
2577 );
2578
2579 CREATE OR REPLACE FUNCTION asset.normalize_affix_sortkey () RETURNS TRIGGER AS $$
2580 BEGIN
2581     NEW.label_sortkey := REGEXP_REPLACE(
2582         evergreen.lpad_number_substrings(
2583             naco_normalize(NEW.label),
2584             '0',
2585             10
2586         ),
2587         E'\\s+',
2588         '',
2589         'g'
2590     );
2591     RETURN NEW;
2592 END;
2593 $$ LANGUAGE PLPGSQL;
2594
2595 CREATE TRIGGER prefix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_prefix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
2596 CREATE UNIQUE INDEX asset_call_number_prefix_once_per_lib ON asset.call_number_prefix (label, owning_lib);
2597 CREATE INDEX asset_call_number_prefix_sortkey_idx ON asset.call_number_prefix (label_sortkey);
2598
2599 CREATE TABLE asset.call_number_suffix (
2600        id                      SERIAL   PRIMARY KEY,
2601        owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
2602        label               TEXT                NOT NULL, -- i18n
2603        label_sortkey   TEXT
2604 );
2605 CREATE TRIGGER suffix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_suffix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
2606 CREATE UNIQUE INDEX asset_call_number_suffix_once_per_lib ON asset.call_number_suffix (label, owning_lib);
2607 CREATE INDEX asset_call_number_suffix_sortkey_idx ON asset.call_number_suffix (label_sortkey);
2608
2609 INSERT INTO asset.call_number_suffix (id, owning_lib, label) VALUES (-1, 1, '');
2610 INSERT INTO asset.call_number_prefix (id, owning_lib, label) VALUES (-1, 1, '');
2611
2612 DROP INDEX IF EXISTS asset.asset_call_number_label_once_per_lib;
2613
2614 ALTER TABLE asset.call_number
2615     ADD COLUMN prefix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_prefix(id) DEFERRABLE INITIALLY DEFERRED,
2616     ADD COLUMN suffix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_suffix(id) DEFERRABLE INITIALLY DEFERRED;
2617
2618 ALTER TABLE auditor.asset_call_number_history
2619     ADD COLUMN prefix INT NOT NULL DEFAULT -1,
2620     ADD COLUMN suffix INT NOT NULL DEFAULT -1;
2621
2622 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;
2623
2624 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
2625     'ui.cat.volume_copy_editor.horizontal',
2626     oils_i18n_gettext(
2627         'ui.cat.volume_copy_editor.horizontal',
2628         'GUI: Horizontal layout for Volume/Copy Creator/Editor.',
2629         'coust', 'label'),
2630     oils_i18n_gettext(
2631         'ui.cat.volume_copy_editor.horizontal',
2632         '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.',
2633         'coust', 'description'),
2634     'bool'
2635 );
2636
2637
2638
2639 -- 0506
2640 ALTER FUNCTION actor.org_unit_descendants( INT, INT ) ROWS 1;
2641 ALTER FUNCTION actor.org_unit_descendants( INT ) ROWS 1;
2642 ALTER FUNCTION actor.org_unit_descendants_distance( INT )  ROWS 1;
2643 ALTER FUNCTION actor.org_unit_ancestors( INT )  ROWS 1;
2644 ALTER FUNCTION actor.org_unit_ancestors_distance( INT )  ROWS 1;
2645 ALTER FUNCTION actor.org_unit_full_path ( INT )  ROWS 2;
2646 ALTER FUNCTION actor.org_unit_full_path ( INT, INT ) ROWS 2;
2647 ALTER FUNCTION actor.org_unit_combined_ancestors ( INT, INT ) ROWS 1;
2648 ALTER FUNCTION actor.org_unit_common_ancestors ( INT, INT ) ROWS 1;
2649 ALTER FUNCTION actor.org_unit_ancestor_setting( TEXT, INT ) ROWS 1;
2650 ALTER FUNCTION permission.grp_ancestors ( INT ) ROWS 1;
2651 ALTER FUNCTION permission.grp_ancestors_distance( INT ) ROWS 1;
2652 ALTER FUNCTION permission.grp_descendants_distance( INT ) ROWS 1;
2653 ALTER FUNCTION permission.usr_perms ( INT ) ROWS 10;
2654 ALTER FUNCTION permission.usr_has_perm_at_nd ( INT, TEXT) ROWS 1;
2655 ALTER FUNCTION permission.usr_has_perm_at_all_nd ( INT, TEXT ) ROWS 1;
2656 ALTER FUNCTION permission.usr_has_perm_at ( INT, TEXT ) ROWS 1;
2657 ALTER FUNCTION permission.usr_has_perm_at_all ( INT, TEXT ) ROWS 1;
2658
2659
2660 CREATE TRIGGER facet_force_nfc_tgr
2661     BEFORE UPDATE OR INSERT ON metabib.facet_entry
2662     FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
2663
2664 DROP FUNCTION IF EXISTS public.force_unicode_normal_form (TEXT,TEXT);
2665 DROP FUNCTION IF EXISTS public.facet_force_nfc ();
2666
2667 DROP TRIGGER b_maintain_901 ON biblio.record_entry;
2668 DROP TRIGGER b_maintain_901 ON authority.record_entry;
2669 DROP TRIGGER b_maintain_901 ON serial.record_entry;
2670
2671 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2672 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2673 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON serial.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2674
2675 DROP FUNCTION IF EXISTS public.maintain_901 ();
2676
2677 ------ Backporting note: 2.1+ only beyond here --------
2678
2679 CREATE SCHEMA unapi;
2680
2681 CREATE TABLE unapi.bre_output_layout (
2682     name                TEXT    PRIMARY KEY,
2683     transform           TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
2684     mime_type           TEXT    NOT NULL,
2685     feed_top            TEXT    NOT NULL,
2686     holdings_element    TEXT,
2687     title_element       TEXT,
2688     description_element TEXT,
2689     creator_element     TEXT,
2690     update_ts_element   TEXT
2691 );
2692
2693 INSERT INTO unapi.bre_output_layout
2694     (name,           transform, mime_type,              holdings_element, feed_top,         title_element, description_element, creator_element, update_ts_element)
2695         VALUES
2696     ('holdings_xml', NULL,      'application/xml',      NULL,             'hxml',           NULL,          NULL,                NULL,            NULL),
2697     ('marcxml',      'marcxml', 'application/marc+xml', 'record',         'collection',     NULL,          NULL,                NULL,            NULL),
2698     ('mods32',       'mods32',  'application/mods+xml', 'mods',           'modsCollection', NULL,          NULL,                NULL,            NULL)
2699 ;
2700
2701 -- Dummy functions, so we can create the real ones out of order
2702 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;
2703 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;
2704 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;
2705 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;
2706 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;
2707 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;
2708 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;
2709 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;
2710 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;
2711 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;
2712 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;
2713 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;
2714 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;
2715 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;
2716 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;
2717 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;
2718 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;
2719 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;
2720 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;
2721 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;
2722 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;
2723
2724 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;
2725 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;
2726
2727 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$
2728 DECLARE
2729     key     TEXT;
2730     output  XML;
2731 BEGIN
2732     key :=
2733         'id'        || COALESCE(obj_id::TEXT,'') ||
2734         'format'    || COALESCE(format::TEXT,'') ||
2735         'ename'     || COALESCE(ename::TEXT,'') ||
2736         'includes'  || COALESCE(includes::TEXT,'{}'::TEXT[]::TEXT) ||
2737         'org'       || COALESCE(org::TEXT,'') ||
2738         'depth'     || COALESCE(depth::TEXT,'') ||
2739         'slimit'    || COALESCE(slimit::TEXT,'') ||
2740         'soffset'   || COALESCE(soffset::TEXT,'') ||
2741         'include_xmlns'   || COALESCE(include_xmlns::TEXT,'');
2742     -- RAISE NOTICE 'memoize key: %', key;
2743
2744     key := MD5(key);
2745     -- RAISE NOTICE 'memoize hash: %', key;
2746
2747     -- XXX cache logic ... memcached? table?
2748
2749     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;
2750     RETURN output;
2751 END;
2752 $F$ LANGUAGE PLPGSQL;
2753
2754 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$
2755 DECLARE
2756     layout          unapi.bre_output_layout%ROWTYPE;
2757     transform       config.xml_transform%ROWTYPE;
2758     item_format     TEXT;
2759     tmp_xml         TEXT;
2760     xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
2761     ouid            INT;
2762     element_list    TEXT[];
2763 BEGIN
2764
2765     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
2766     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
2767
2768     IF layout.name IS NULL THEN
2769         RETURN NULL::XML;
2770     END IF;
2771
2772     SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
2773     xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
2774
2775     -- Gather the bib xml
2776     SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
2777
2778     IF layout.title_element IS NOT NULL THEN
2779         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;
2780     END IF;
2781
2782     IF layout.description_element IS NOT NULL THEN
2783         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;
2784     END IF;
2785
2786     IF layout.creator_element IS NOT NULL THEN
2787         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;
2788     END IF;
2789
2790     IF layout.update_ts_element IS NOT NULL THEN
2791         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;
2792     END IF;
2793
2794     IF unapi_url IS NOT NULL THEN
2795         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;
2796     END IF;
2797
2798     IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
2799
2800     element_list := regexp_split_to_array(layout.feed_top,E'\\.');
2801     FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
2802         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;
2803     END LOOP;
2804
2805     RETURN tmp_xml::XML;
2806 END;
2807 $F$ LANGUAGE PLPGSQL;
2808
2809 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$
2810 DECLARE
2811     me      biblio.record_entry%ROWTYPE;
2812     layout  unapi.bre_output_layout%ROWTYPE;
2813     xfrm    config.xml_transform%ROWTYPE;
2814     ouid    INT;
2815     tmp_xml TEXT;
2816     top_el  TEXT;
2817     output  XML;
2818     hxml    XML;
2819 BEGIN
2820
2821     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
2822
2823     IF ouid IS NULL THEN
2824         RETURN NULL::XML;
2825     END IF;
2826
2827     IF format = 'holdings_xml' THEN -- the special case
2828         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
2829         RETURN output;
2830     END IF;
2831
2832     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
2833
2834     IF layout.name IS NULL THEN
2835         RETURN NULL::XML;
2836     END IF;
2837
2838     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
2839
2840     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
2841
2842     -- grab hodlings if we need them
2843     IF ('holdings_xml' = ANY (includes)) THEN 
2844         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
2845     ELSE
2846         hxml := NULL::XML;
2847     END IF;
2848
2849
2850     -- generate our item node
2851
2852
2853     IF format = 'marcxml' THEN
2854         tmp_xml := me.marc;
2855         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
2856            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
2857         END IF; 
2858     ELSE
2859         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
2860     END IF;
2861
2862     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
2863
2864     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
2865         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
2866     END IF;
2867
2868     IF ('bre.unapi' = ANY (includes)) THEN 
2869         output := REGEXP_REPLACE(
2870             tmp_xml,
2871             '</' || top_el || '>(.*?)',
2872             XMLELEMENT(
2873                 name abbr,
2874                 XMLATTRIBUTES(
2875                     'http://www.w3.org/1999/xhtml' AS xmlns,
2876                     'unapi-id' AS class,
2877                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
2878                 )
2879             )::TEXT || '</' || top_el || E'>\\1'
2880         );
2881     ELSE
2882         output := tmp_xml;
2883     END IF;
2884
2885     RETURN output;
2886 END;
2887 $F$ LANGUAGE PLPGSQL;
2888
2889 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$
2890      SELECT  XMLELEMENT(
2891                  name holdings,
2892                  XMLATTRIBUTES(
2893                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2894                     CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
2895                  ),
2896                  XMLELEMENT(
2897                      name counts,
2898                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
2899                          SELECT  XMLELEMENT(
2900                                      name count,
2901                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2902                                  )::text
2903                            FROM  asset.opac_ou_record_copy_count($2,  $1)
2904                                      UNION
2905                          SELECT  XMLELEMENT(
2906                                      name count,
2907                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2908                                  )::text
2909                            FROM  asset.staff_ou_record_copy_count($2, $1)
2910                                      ORDER BY 1
2911                      )x)
2912                  ),
2913                  CASE 
2914                      WHEN ('bmp' = ANY ($5)) THEN
2915                         XMLELEMENT( name monograph_parts,
2916                             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))
2917                         )
2918                      ELSE NULL
2919                  END,
2920                  CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
2921                      XMLELEMENT(
2922                          name volumes,
2923                          (SELECT XMLAGG(acn) FROM (
2924                             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)
2925                               FROM  asset.call_number acn
2926                               WHERE acn.record = $1
2927                                     AND EXISTS (
2928                                         SELECT  1
2929                                           FROM  asset.copy acp
2930                                                 JOIN actor.org_unit_descendants(
2931                                                     $2,
2932                                                     (COALESCE(
2933                                                         $4,
2934                                                         (SELECT aout.depth
2935                                                           FROM  actor.org_unit_type aout
2936                                                                 JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
2937                                                         )
2938                                                     ))
2939                                                 ) aoud ON (acp.circ_lib = aoud.id)
2940                                           LIMIT 1
2941                                     )
2942                               ORDER BY label_sortkey
2943                               LIMIT $6
2944                               OFFSET $7
2945                          )x)
2946                      )
2947                  ELSE NULL END,
2948                  CASE WHEN ('ssub' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
2949                      XMLELEMENT(
2950                          name subscriptions,
2951                          (SELECT XMLAGG(ssub) FROM (
2952                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
2953                               FROM  serial.subscription
2954                               WHERE record_entry = $1
2955                         )x)
2956                      )
2957                  ELSE NULL END
2958              );
2959 $F$ LANGUAGE SQL;
2960
2961 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$
2962         SELECT  XMLELEMENT(
2963                     name subscription,
2964                     XMLATTRIBUTES(
2965                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2966                         'tag:open-ils.org:U2@ssub/' || id AS id,
2967                         start_date AS start, end_date AS end, expected_date_offset
2968                     ),
2969                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8),
2970                     XMLELEMENT( name distributions,
2971                         CASE 
2972                             WHEN ('sdist' = ANY ($4)) THEN
2973                                 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))
2974                             ELSE NULL
2975                         END
2976                     )
2977                 )
2978           FROM  serial.subscription ssub
2979           WHERE id = $1
2980           GROUP BY id, start_date, end_date, expected_date_offset, owning_lib;
2981 $F$ LANGUAGE SQL;
2982
2983 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$
2984         SELECT  XMLELEMENT(
2985                     name distribution,
2986                     XMLATTRIBUTES(
2987                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2988                         'tag:open-ils.org:U2@sdist/' || id AS id,
2989                         'tag:open-ils.org:U2@acn/' || receive_call_number AS receive_call_number,
2990                         'tag:open-ils.org:U2@acn/' || bind_call_number AS bind_call_number,
2991                         unit_label_prefix, label, unit_label_suffix, summary_method
2992                     ),
2993                     unapi.aou( holding_lib, $2, 'holding_lib', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8),
2994                     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,
2995                     XMLELEMENT( name streams,
2996                         CASE 
2997                             WHEN ('sstr' = ANY ($4)) THEN
2998                                 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))
2999                             ELSE NULL
3000                         END
3001                     ),
3002                     XMLELEMENT( name summaries,
3003                         CASE 
3004                             WHEN ('ssum' = ANY ($4)) THEN
3005                                 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))
3006                             ELSE NULL
3007                         END,
3008                         CASE 
3009                             WHEN ('ssum' = ANY ($4)) THEN
3010                                 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))
3011                             ELSE NULL
3012                         END,
3013                         CASE 
3014                             WHEN ('ssum' = ANY ($4)) THEN
3015                                 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))
3016                             ELSE NULL
3017                         END
3018                     )
3019                 )
3020           FROM  serial.distribution sdist
3021           WHERE id = $1
3022           GROUP BY id, label, unit_label_prefix, unit_label_suffix, holding_lib, summary_method, subscription, receive_call_number, bind_call_number;
3023 $F$ LANGUAGE SQL;
3024
3025 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$
3026     SELECT  XMLELEMENT(
3027                 name stream,
3028                 XMLATTRIBUTES(
3029                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3030                     'tag:open-ils.org:U2@sstr/' || id AS id,
3031                     routing_label
3032                 ),
3033                 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,
3034                 XMLELEMENT( name items,
3035                     CASE 
3036                         WHEN ('sitem' = ANY ($4)) THEN
3037                             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))
3038                         ELSE NULL
3039                     END
3040                 )
3041             )
3042       FROM  serial.stream sstr
3043       WHERE id = $1
3044       GROUP BY id, routing_label, distribution;
3045 $F$ LANGUAGE SQL;
3046
3047 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$
3048     SELECT  XMLELEMENT(
3049                 name issuance,
3050                 XMLATTRIBUTES(
3051                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3052                     'tag:open-ils.org:U2@siss/' || id AS id,
3053                     create_date, edit_date, label, date_published,
3054                     holding_code, holding_type, holding_link_id
3055                 ),
3056                 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,
3057                 XMLELEMENT( name items,
3058                     CASE 
3059                         WHEN ('sitem' = ANY ($4)) THEN
3060                             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))
3061                         ELSE NULL
3062                     END
3063                 )
3064             )
3065       FROM  serial.issuance sstr
3066       WHERE id = $1
3067       GROUP BY id, create_date, edit_date, label, date_published, holding_code, holding_type, holding_link_id, subscription;
3068 $F$ LANGUAGE SQL;
3069
3070 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$
3071         SELECT  XMLELEMENT(
3072                     name serial_item,
3073                     XMLATTRIBUTES(
3074                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3075                         'tag:open-ils.org:U2@sitem/' || id AS id,
3076                         'tag:open-ils.org:U2@siss/' || issuance AS issuance,
3077                         date_expected, date_received
3078                     ),
3079                     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,
3080                     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,
3081                     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,
3082                     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
3083 --                    XMLELEMENT( name notes,
3084 --                        CASE 
3085 --                            WHEN ('acpn' = ANY ($4)) THEN
3086 --                                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))
3087 --                            ELSE NULL
3088 --                        END
3089 --                    )
3090                 )
3091           FROM  serial.item sitem
3092           WHERE id = $1;
3093 $F$ LANGUAGE SQL;
3094
3095
3096 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$
3097     SELECT  XMLELEMENT(
3098                 name serial_summary,
3099                 XMLATTRIBUTES(
3100                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3101                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3102                     'sssum' AS type, generated_coverage, textual_holdings, show_generated
3103                 ),
3104                 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
3105             )
3106       FROM  serial.supplement_summary ssum
3107       WHERE id = $1
3108       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3109 $F$ LANGUAGE SQL;
3110
3111 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$
3112     SELECT  XMLELEMENT(
3113                 name serial_summary,
3114                 XMLATTRIBUTES(
3115                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3116                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3117                     'sbsum' AS type, generated_coverage, textual_holdings, show_generated
3118                 ),
3119                 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
3120             )
3121       FROM  serial.basic_summary ssum
3122       WHERE id = $1
3123       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3124 $F$ LANGUAGE SQL;
3125
3126 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$
3127     SELECT  XMLELEMENT(
3128                 name serial_summary,
3129                 XMLATTRIBUTES(
3130                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3131                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3132                     'sisum' AS type, generated_coverage, textual_holdings, show_generated
3133                 ),
3134                 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
3135             )
3136       FROM  serial.index_summary ssum
3137       WHERE id = $1
3138       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3139 $F$ LANGUAGE SQL;
3140
3141
3142 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$
3143 DECLARE
3144     output XML;
3145 BEGIN
3146     IF ename = 'circlib' THEN
3147         SELECT  XMLELEMENT(
3148                     name circlib,
3149                     XMLATTRIBUTES(
3150                         'http://open-ils.org/spec/actors/v1' AS xmlns,
3151                         id AS ident
3152                     ),
3153                     name
3154                 ) INTO output
3155           FROM  actor.org_unit aou
3156           WHERE id = obj_id;
3157     ELSE
3158         EXECUTE $$SELECT  XMLELEMENT(
3159                     name $$ || ename || $$,
3160                     XMLATTRIBUTES(
3161                         'http://open-ils.org/spec/actors/v1' AS xmlns,
3162                         'tag:open-ils.org:U2@aou/' || id AS id,
3163                         shortname, name, opac_visible
3164                     )
3165                 )
3166           FROM  actor.org_unit aou
3167          WHERE id = $1 $$ INTO output USING obj_id;
3168     END IF;
3169
3170     RETURN output;
3171
3172 END;
3173 $F$ LANGUAGE PLPGSQL;
3174
3175 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$
3176     SELECT  XMLELEMENT(
3177                 name location,
3178                 XMLATTRIBUTES(
3179                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3180                     id AS ident
3181                 ),
3182                 name
3183             )
3184       FROM  asset.copy_location
3185       WHERE id = $1;
3186 $F$ LANGUAGE SQL;
3187
3188 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$
3189     SELECT  XMLELEMENT(
3190                 name status,
3191                 XMLATTRIBUTES(
3192                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3193                     id AS ident
3194                 ),
3195                 name
3196             )
3197       FROM  config.copy_status
3198       WHERE id = $1;
3199 $F$ LANGUAGE SQL;
3200
3201 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$
3202         SELECT  XMLELEMENT(
3203                     name copy_note,
3204                     XMLATTRIBUTES(
3205                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3206                         create_date AS date,
3207                         title
3208                     ),
3209                     value
3210                 )
3211           FROM  asset.copy_note
3212           WHERE id = $1;
3213 $F$ LANGUAGE SQL;
3214
3215 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$
3216         SELECT  XMLELEMENT(
3217                     name statcat,
3218                     XMLATTRIBUTES(
3219                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3220                         sc.name,
3221                         sc.opac_visible
3222                     ),
3223                     asce.value
3224                 )
3225           FROM  asset.stat_cat_entry asce
3226                 JOIN asset.stat_cat sc ON (sc.id = asce.stat_cat)
3227           WHERE asce.id = $1;
3228 $F$ LANGUAGE SQL;
3229
3230 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$
3231         SELECT  XMLELEMENT(
3232                     name monograph_part,
3233                     XMLATTRIBUTES(
3234                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3235                         'tag:open-ils.org:U2@bmp/' || id AS id,
3236                         id AS ident,
3237                         label,
3238                         label_sortkey,
3239                         'tag:open-ils.org:U2@bre/' || record AS record
3240                     ),
3241                     CASE 
3242                         WHEN ('acp' = ANY ($4)) THEN
3243                             XMLELEMENT( name copies,
3244                                 (SELECT XMLAGG(acp) FROM (
3245                                     SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE)
3246                                       FROM  asset.copy cp
3247                                             JOIN asset.copy_part_map cpm ON (cpm.target_copy = cp.id)
3248                                       WHERE cpm.part = $1
3249                                       ORDER BY COALESCE(cp.copy_number,0), cp.barcode
3250                                       LIMIT $7
3251                                       OFFSET $8
3252                                 )x)
3253                             )
3254                         ELSE NULL
3255                     END,
3256                     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
3257                 )
3258           FROM  biblio.monograph_part
3259           WHERE id = $1
3260           GROUP BY id, label, label_sortkey, record;
3261 $F$ LANGUAGE SQL;
3262
3263 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$
3264         SELECT  XMLELEMENT(
3265                     name copy,
3266                     XMLATTRIBUTES(
3267                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3268                         'tag:open-ils.org:U2@acp/' || id AS id,
3269                         create_date, edit_date, copy_number, circulate, deposit,
3270                         ref, holdable, deleted, deposit_amount, price, barcode,
3271                         circ_modifier, circ_as_type, opac_visible
3272                     ),
3273                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3274                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3275                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3276                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3277                     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,
3278                     XMLELEMENT( name copy_notes,
3279                         CASE 
3280                             WHEN ('acpn' = ANY ($4)) THEN
3281                                 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))
3282                             ELSE NULL
3283                         END
3284                     ),
3285                     XMLELEMENT( name statcats,
3286                         CASE 
3287                             WHEN ('ascecm' = ANY ($4)) THEN
3288                                 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))
3289                             ELSE NULL
3290                         END
3291                     ),
3292                     CASE 
3293                         WHEN ('bmp' = ANY ($4)) THEN
3294                             XMLELEMENT( name monograph_parts,
3295                                 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))
3296                             )
3297                         ELSE NULL
3298                     END
3299                 )
3300           FROM  asset.copy cp
3301           WHERE id = $1
3302           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;
3303 $F$ LANGUAGE SQL;
3304
3305 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$
3306         SELECT  XMLELEMENT(
3307                     name serial_unit,
3308                     XMLATTRIBUTES(
3309                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3310                         'tag:open-ils.org:U2@acp/' || id AS id,
3311                         create_date, edit_date, copy_number, circulate, deposit,
3312                         ref, holdable, deleted, deposit_amount, price, barcode,
3313                         circ_modifier, circ_as_type, opac_visible, status_changed_time,
3314                         floating, mint_condition, detailed_contents, sort_key, summary_contents, cost 
3315                     ),
3316                     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),
3317                     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),
3318                     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),
3319                     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),
3320                     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,
3321                     XMLELEMENT( name copy_notes,
3322                         CASE 
3323                             WHEN ('acpn' = ANY ($4)) THEN
3324                                 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))
3325                             ELSE NULL
3326                         END
3327                     ),
3328                     XMLELEMENT( name statcats,
3329                         CASE 
3330                             WHEN ('ascecm' = ANY ($4)) THEN
3331                                 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))
3332                             ELSE NULL
3333                         END
3334                     )
3335                 )
3336           FROM  serial.unit cp
3337           WHERE id = $1
3338           GROUP BY  id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, floating, mint_condition,
3339                     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;
3340 $F$ LANGUAGE SQL;
3341
3342 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$
3343         SELECT  XMLELEMENT(
3344                     name volume,
3345                     XMLATTRIBUTES(
3346                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3347                         'tag:open-ils.org:U2@acn/' || acn.id AS id,
3348                         o.shortname AS lib,
3349                         o.opac_visible AS opac_visible,
3350                         deleted, label, label_sortkey, label_class, record
3351                     ),
3352                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8),
3353                     XMLELEMENT( name copies,
3354                         CASE 
3355                             WHEN ('acp' = ANY ($4)) THEN
3356                                 (SELECT XMLAGG(acp) FROM (
3357                                     SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE)
3358                                       FROM  asset.copy cp
3359                                             JOIN actor.org_unit_descendants(
3360                                                 (SELECT id FROM actor.org_unit WHERE shortname = $5),
3361                                                 (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))))
3362                                             ) aoud ON (cp.circ_lib = aoud.id)
3363                                       WHERE cp.call_number = acn.id
3364                                       ORDER BY COALESCE(cp.copy_number,0), cp.barcode
3365                                       LIMIT $7
3366                                       OFFSET $8
3367                                 )x)
3368                             ELSE NULL
3369                         END
3370                     ),
3371                     XMLELEMENT(
3372                         name uris,
3373                         (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)
3374                     ),
3375                     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,
3376                     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,
3377                     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
3378                 ) AS x
3379           FROM  asset.call_number acn
3380                 JOIN actor.org_unit o ON (o.id = acn.owning_lib)
3381           WHERE acn.id = $1
3382           GROUP BY acn.id, o.shortname, o.opac_visible, deleted, label, label_sortkey, label_class, owning_lib, record, acn.prefix, acn.suffix;
3383 $F$ LANGUAGE SQL;
3384
3385 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$
3386         SELECT  XMLELEMENT(
3387                     name call_number_prefix,
3388                     XMLATTRIBUTES(
3389                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3390                         id AS ident,
3391                         label,
3392                         label_sortkey
3393                     ),
3394                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acnp'), $5, $6, $7, $8)
3395                 )
3396           FROM  asset.call_number_prefix
3397           WHERE id = $1;
3398 $F$ LANGUAGE SQL;
3399
3400 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$
3401         SELECT  XMLELEMENT(
3402                     name call_number_suffix,
3403                     XMLATTRIBUTES(
3404                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3405                         id AS ident,
3406                         label,
3407                         label_sortkey
3408                     ),
3409                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acns'), $5, $6, $7, $8)
3410                 )
3411           FROM  asset.call_number_suffix
3412           WHERE id = $1;
3413 $F$ LANGUAGE SQL;
3414
3415 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$
3416         SELECT  XMLELEMENT(
3417                     name volume,
3418                     XMLATTRIBUTES(
3419                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3420                         'tag:open-ils.org:U2@auri/' || uri.id AS id,
3421                         use_restriction,
3422                         href,
3423                         label
3424                     ),
3425                     XMLELEMENT( name copies,
3426                         CASE 
3427                             WHEN ('acn' = ANY ($4)) THEN
3428                                 (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)
3429                             ELSE NULL
3430                         END
3431                     )
3432                 ) AS x
3433           FROM  asset.uri uri
3434           WHERE uri.id = $1
3435           GROUP BY uri.id, use_restriction, href, label;
3436 $F$ LANGUAGE SQL;
3437
3438 DROP FUNCTION IF EXISTS public.array_remove_item_by_value(ANYARRAY,ANYELEMENT);
3439
3440 DROP FUNCTION IF EXISTS public.lpad_number_substrings(TEXT,TEXT,INT);
3441
3442
3443 -- 0511
3444 CREATE OR REPLACE FUNCTION evergreen.fake_fkey_tgr () RETURNS TRIGGER AS $F$
3445 DECLARE
3446     copy_id BIGINT;
3447 BEGIN
3448     EXECUTE 'SELECT ($1).' || quote_ident(TG_ARGV[0]) INTO copy_id USING NEW;
3449     PERFORM * FROM asset.copy WHERE id = copy_id;
3450     IF NOT FOUND THEN
3451         RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
3452     END IF;
3453     RETURN NULL;
3454 END;
3455 $F$ LANGUAGE PLPGSQL;
3456
3457 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');
3458
3459 -- 0512
3460 CREATE TABLE biblio.peer_type (
3461     id      SERIAL  PRIMARY KEY,
3462     name        TEXT        NOT NULL UNIQUE -- i18n
3463 );
3464
3465 CREATE TABLE biblio.peer_bib_copy_map (
3466     id      SERIAL  PRIMARY KEY,
3467     peer_type   INT     NOT NULL REFERENCES biblio.peer_type (id),
3468     peer_record BIGINT      NOT NULL REFERENCES biblio.record_entry (id),
3469     target_copy BIGINT      NOT NULL -- can't use fkey because of acp subtables
3470 );
3471 CREATE INDEX peer_bib_copy_map_record_idx ON biblio.peer_bib_copy_map (peer_record);
3472 CREATE INDEX peer_bib_copy_map_copy_idx ON biblio.peer_bib_copy_map (target_copy);
3473
3474 DROP TABLE asset.opac_visible_copies;
3475 CREATE TABLE asset.opac_visible_copies (
3476   id        BIGSERIAL primary key,
3477   copy_id   BIGINT,
3478   record    BIGINT,
3479   circ_lib  INTEGER
3480 );
3481
3482 INSERT INTO biblio.peer_type (id,name) VALUES
3483     (1,oils_i18n_gettext(1,'Bound Volume','bpt','name')),
3484     (2,oils_i18n_gettext(2,'Bilingual','bpt','name')),
3485     (3,oils_i18n_gettext(3,'Back-to-back','bpt','name')),
3486     (4,oils_i18n_gettext(4,'Set','bpt','name')),
3487     (5,oils_i18n_gettext(5,'e-Reader Preload','bpt','name')); 
3488
3489 SELECT SETVAL('biblio.peer_type_id_seq'::TEXT, 100);
3490
3491 CREATE OR REPLACE FUNCTION search.query_parser_fts (
3492
3493     param_search_ou INT,
3494     param_depth     INT,
3495     param_query     TEXT,
3496     param_statuses  INT[],
3497     param_locations INT[],
3498     param_offset    INT,
3499     param_check     INT,
3500     param_limit     INT,
3501     metarecord      BOOL,
3502     staff           BOOL
3503  
3504 ) RETURNS SETOF search.search_result AS $func$
3505 DECLARE
3506
3507     current_res         search.search_result%ROWTYPE;
3508     search_org_list     INT[];
3509
3510     check_limit         INT;
3511     core_limit          INT;
3512     core_offset         INT;
3513     tmp_int             INT;
3514
3515     core_result         RECORD;
3516     core_cursor         REFCURSOR;
3517     core_rel_query      TEXT;
3518
3519     total_count         INT := 0;
3520     check_count         INT := 0;
3521     deleted_count       INT := 0;
3522     visible_count       INT := 0;
3523     excluded_count      INT := 0;
3524
3525 BEGIN
3526
3527     check_limit := COALESCE( param_check, 1000 );
3528     core_limit  := COALESCE( param_limit, 25000 );
3529     core_offset := COALESCE( param_offset, 0 );
3530
3531     -- core_skip_chk := COALESCE( param_skip_chk, 1 );
3532
3533     IF param_search_ou > 0 THEN
3534         IF param_depth IS NOT NULL THEN
3535             SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
3536         ELSE
3537             SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
3538         END IF;
3539     ELSIF param_search_ou < 0 THEN
3540         SELECT array_accum(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
3541     ELSIF param_search_ou = 0 THEN
3542         -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
3543     END IF;
3544
3545     OPEN core_cursor FOR EXECUTE param_query;
3546
3547     LOOP
3548
3549         FETCH core_cursor INTO core_result;
3550         EXIT WHEN NOT FOUND;
3551         EXIT WHEN total_count >= core_limit;
3552
3553         total_count := total_count + 1;
3554
3555         CONTINUE WHEN total_count NOT BETWEEN  core_offset + 1 AND check_limit + core_offset;
3556
3557         check_count := check_count + 1;
3558
3559         PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
3560         IF NOT FOUND THEN
3561             -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
3562             deleted_count := deleted_count + 1;
3563             CONTINUE;
3564         END IF;
3565
3566         PERFORM 1
3567           FROM  biblio.record_entry b
3568                 JOIN config.bib_source s ON (b.source = s.id)
3569           WHERE s.transcendant
3570                 AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
3571
3572         IF FOUND THEN
3573             -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
3574             visible_count := visible_count + 1;
3575
3576             current_res.id = core_result.id;
3577             current_res.rel = core_result.rel;
3578
3579             tmp_int := 1;
3580             IF metarecord THEN
3581                 SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
3582             END IF;
3583
3584             IF tmp_int = 1 THEN
3585                 current_res.record = core_result.records[1];
3586             ELSE
3587                 current_res.record = NULL;
3588             END IF;
3589
3590             RETURN NEXT current_res;
3591
3592             CONTINUE;
3593         END IF;
3594
3595         PERFORM 1
3596           FROM  asset.call_number cn
3597                 JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
3598                 JOIN asset.uri uri ON (map.uri = uri.id)
3599           WHERE NOT cn.deleted
3600                 AND cn.label = '##URI##'
3601                 AND uri.active
3602                 AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
3603                 AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
3604                 AND cn.owning_lib IN ( SELECT * FROM unnest( search_org_list ) )
3605           LIMIT 1;
3606
3607         IF FOUND THEN
3608             -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
3609             visible_count := visible_count + 1;
3610
3611             current_res.id = core_result.id;
3612             current_res.rel = core_result.rel;
3613
3614             tmp_int := 1;
3615             IF metarecord THEN
3616                 SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
3617             END IF;
3618
3619             IF tmp_int = 1 THEN
3620                 current_res.record = core_result.records[1];
3621             ELSE
3622                 current_res.record = NULL;
3623             END IF;
3624
3625             RETURN NEXT current_res;
3626
3627             CONTINUE;
3628         END IF;
3629
3630         IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
3631
3632             PERFORM 1
3633               FROM  asset.call_number cn
3634                     JOIN asset.copy cp ON (cp.call_number = cn.id)
3635               WHERE NOT cn.deleted
3636                     AND NOT cp.deleted
3637                     AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
3638                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
3639                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
3640               LIMIT 1;
3641
3642             IF NOT FOUND THEN
3643                 PERFORM 1
3644                   FROM  biblio.peer_bib_copy_map pr
3645                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
3646                   WHERE NOT cp.deleted
3647                         AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
3648                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
3649                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
3650                   LIMIT 1;
3651
3652                 IF NOT FOUND THEN
3653                 -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
3654                     excluded_count := excluded_count + 1;
3655                     CONTINUE;
3656                 END IF;
3657             END IF;
3658
3659         END IF;
3660
3661         IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
3662
3663             PERFORM 1
3664               FROM  asset.call_number cn
3665                     JOIN asset.copy cp ON (cp.call_number = cn.id)
3666               WHERE NOT cn.deleted
3667                     AND NOT cp.deleted
3668                     AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
3669                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
3670                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
3671               LIMIT 1;
3672
3673             IF NOT FOUND THEN
3674                 PERFORM 1
3675                   FROM  biblio.peer_bib_copy_map pr
3676                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
3677                   WHERE NOT cp.deleted
3678                         AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
3679                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
3680                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
3681                   LIMIT 1;
3682
3683                 IF NOT FOUND THEN
3684                     -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
3685                     excluded_count := excluded_count + 1;
3686                     CONTINUE;
3687                 END IF;
3688             END IF;
3689
3690         END IF;
3691
3692         IF staff IS NULL OR NOT staff THEN
3693
3694             PERFORM 1
3695               FROM  asset.opac_visible_copies
3696               WHERE circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
3697                     AND record IN ( SELECT * FROM unnest( core_result.records ) )
3698               LIMIT 1;
3699
3700             IF NOT FOUND THEN
3701                 PERFORM 1
3702                   FROM  biblio.peer_bib_copy_map pr
3703                         JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
3704                   WHERE cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
3705                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
3706                   LIMIT 1;
3707
3708                 IF NOT FOUND THEN
3709
3710                     -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
3711                     excluded_count := excluded_count + 1;
3712                     CONTINUE;
3713                 END IF;
3714             END IF;
3715
3716         ELSE
3717
3718             PERFORM 1
3719               FROM  asset.call_number cn
3720                     JOIN asset.copy cp ON (cp.call_number = cn.id)
3721               WHERE NOT cn.deleted
3722                     AND NOT cp.deleted
3723                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
3724                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
3725               LIMIT 1;
3726
3727             IF NOT FOUND THEN
3728
3729                 PERFORM 1
3730                   FROM  biblio.peer_bib_copy_map pr
3731                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
3732                   WHERE NOT cp.deleted
3733                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
3734                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
3735                   LIMIT 1;
3736
3737                 IF NOT FOUND THEN
3738
3739                     PERFORM 1
3740                       FROM  asset.call_number cn
3741                       WHERE cn.record IN ( SELECT * FROM unnest( core_result.records ) )
3742                       LIMIT 1;
3743
3744                     IF FOUND THEN
3745                         -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
3746                         excluded_count := excluded_count + 1;
3747                         CONTINUE;
3748                     END IF;
3749                 END IF;
3750
3751             END IF;
3752
3753         END IF;
3754
3755         visible_count := visible_count + 1;
3756
3757         current_res.id = core_result.id;
3758         current_res.rel = core_result.rel;
3759
3760         tmp_int := 1;
3761         IF metarecord THEN
3762             SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
3763         END IF;
3764
3765         IF tmp_int = 1 THEN
3766             current_res.record = core_result.records[1];
3767         ELSE
3768             current_res.record = NULL;
3769         END IF;
3770
3771         RETURN NEXT current_res;
3772
3773         IF visible_count % 1000 = 0 THEN
3774             -- RAISE NOTICE ' % visible so far ... ', visible_count;
3775         END IF;
3776
3777     END LOOP;
3778
3779     current_res.id = NULL;
3780     current_res.rel = NULL;
3781     current_res.record = NULL;
3782     current_res.total = total_count;
3783     current_res.checked = check_count;
3784     current_res.deleted = deleted_count;
3785     current_res.visible = visible_count;
3786     current_res.excluded = excluded_count;
3787
3788     CLOSE core_cursor;
3789
3790     RETURN NEXT current_res;
3791
3792 END;
3793 $func$ LANGUAGE PLPGSQL;
3794
3795 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$
3796      SELECT  XMLELEMENT(
3797                  name holdings,
3798                  XMLATTRIBUTES(
3799                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3800                     CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
3801                  ),
3802                  XMLELEMENT(
3803                      name counts,
3804                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
3805                          SELECT  XMLELEMENT(
3806                                      name count,
3807                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
3808                                  )::text
3809                            FROM  asset.opac_ou_record_copy_count($2,  $1)
3810                                      UNION
3811                          SELECT  XMLELEMENT(
3812                                      name count,
3813                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
3814                                  )::text
3815                            FROM  asset.staff_ou_record_copy_count($2, $1)
3816                                      ORDER BY 1
3817                      )x)
3818                  ),
3819                  CASE
3820                      WHEN ('bmp' = ANY ($5)) THEN
3821                         XMLELEMENT( name monograph_parts,
3822                             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))
3823                         )
3824                      ELSE NULL
3825                  END,
3826                  CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN
3827                      XMLELEMENT(
3828                          name volumes,
3829                          (SELECT XMLAGG(acn) FROM (
3830                             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)
3831                               FROM  asset.call_number acn
3832                               WHERE acn.record = $1
3833                                     AND EXISTS (
3834                                         SELECT  1
3835                                           FROM  asset.copy acp
3836                                                 JOIN actor.org_unit_descendants(
3837                                                     $2,
3838                                                     (COALESCE(
3839                                                         $4,
3840                                                         (SELECT aout.depth
3841                                                           FROM  actor.org_unit_type aout
3842                                                                 JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
3843                                                         )
3844                                                     ))
3845                                                 ) aoud ON (acp.circ_lib = aoud.id)
3846                                           LIMIT 1
3847                                     )
3848                               ORDER BY label_sortkey
3849                               LIMIT $6
3850                               OFFSET $7
3851                          )x)
3852                      )
3853                  ELSE NULL END,
3854                  CASE WHEN ('ssub' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN
3855                      XMLELEMENT(
3856                          name subscriptions,
3857                          (SELECT XMLAGG(ssub) FROM (
3858                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
3859                               FROM  serial.subscription
3860                               WHERE record_entry = $1
3861                         )x)
3862                      )
3863                  ELSE NULL END,
3864                  CASE WHEN ('acp' = ANY ($5)) THEN
3865                      XMLELEMENT(
3866                          name foreign_copies,
3867                          (SELECT XMLAGG(acp) FROM (
3868                             SELECT  unapi.acp(p.target_copy,'xml','copy','{}'::TEXT[], $3, $4, $6, $7, FALSE)
3869                               FROM  biblio.peer_bib_copy_map p
3870                                     JOIN asset.copy c ON (p.target_copy = c.id)
3871                               WHERE NOT c.deleted AND peer_record = $1
3872                         )x)
3873                      )
3874                  ELSE NULL END
3875              );
3876 $F$ LANGUAGE SQL;
3877
3878 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$
3879         SELECT  XMLELEMENT(
3880                     name copy,
3881                     XMLATTRIBUTES(
3882                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3883                         'tag:open-ils.org:U2@acp/' || id AS id,
3884                         create_date, edit_date, copy_number, circulate, deposit,
3885                         ref, holdable, deleted, deposit_amount, price, barcode,
3886                         circ_modifier, circ_as_type, opac_visible
3887                     ),
3888                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3889                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3890                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3891                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3892                     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,
3893                     XMLELEMENT( name copy_notes,
3894                         CASE
3895                             WHEN ('acpn' = ANY ($4)) THEN
3896                                 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))
3897                             ELSE NULL
3898                         END
3899                     ),
3900                     XMLELEMENT( name statcats,
3901                         CASE
3902                             WHEN ('ascecm' = ANY ($4)) THEN
3903                                 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))
3904                             ELSE NULL
3905                         END
3906                     ),
3907                     XMLELEMENT( name foreign_records,
3908                         CASE
3909                             WHEN ('bre' = ANY ($4)) THEN
3910                                 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))
3911                             ELSE NULL
3912                         END
3913
3914                     ),
3915                     CASE
3916                         WHEN ('bmp' = ANY ($4)) THEN
3917                             XMLELEMENT( name monograph_parts,
3918                                 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))
3919                             )
3920                         ELSE NULL
3921                     END
3922                 )
3923           FROM  asset.copy cp
3924           WHERE id = $1
3925           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;
3926 $F$ LANGUAGE SQL;
3927
3928 CREATE OR REPLACE FUNCTION asset.refresh_opac_visible_copies_mat_view () RETURNS VOID AS $$
3929
3930     TRUNCATE TABLE asset.opac_visible_copies;
3931
3932     INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
3933     SELECT  cp.id, cp.circ_lib, cn.record
3934     FROM  asset.copy cp
3935         JOIN asset.call_number cn ON (cn.id = cp.call_number)
3936         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3937         JOIN asset.copy_location cl ON (cp.location = cl.id)
3938         JOIN config.copy_status cs ON (cp.status = cs.id)
3939         JOIN biblio.record_entry b ON (cn.record = b.id)
3940     WHERE NOT cp.deleted
3941         AND NOT cn.deleted
3942         AND NOT b.deleted
3943         AND cs.opac_visible
3944         AND cl.opac_visible
3945         AND cp.opac_visible
3946         AND a.opac_visible
3947             UNION
3948     SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record
3949     FROM  asset.copy cp
3950         JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
3951         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3952         JOIN asset.copy_location cl ON (cp.location = cl.id)
3953         JOIN config.copy_status cs ON (cp.status = cs.id)
3954     WHERE NOT cp.deleted
3955         AND cs.opac_visible
3956         AND cl.opac_visible
3957         AND cp.opac_visible
3958         AND a.opac_visible;
3959
3960 $$ LANGUAGE SQL;
3961 COMMENT ON FUNCTION asset.refresh_opac_visible_copies_mat_view() IS $$
3962 Rebuild the copy OPAC visibility cache.  Useful during migrations.
3963 $$;
3964
3965 SELECT asset.refresh_opac_visible_copies_mat_view();
3966 CREATE INDEX opac_visible_copies_idx1 on asset.opac_visible_copies (record, circ_lib);
3967 CREATE INDEX opac_visible_copies_copy_id_idx on asset.opac_visible_copies (copy_id);
3968 CREATE UNIQUE INDEX opac_visible_copies_once_per_record_idx on asset.opac_visible_copies (copy_id, record);
3969  
3970 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
3971 DECLARE
3972     add_query       TEXT;
3973     remove_query    TEXT;
3974     do_add          BOOLEAN := false;
3975     do_remove       BOOLEAN := false;
3976 BEGIN
3977     add_query := $$
3978             INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
3979               SELECT id, circ_lib, record FROM (
3980                 SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number
3981                   FROM  asset.copy cp
3982                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
3983                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3984                         JOIN asset.copy_location cl ON (cp.location = cl.id)
3985                         JOIN config.copy_status cs ON (cp.status = cs.id)
3986                         JOIN biblio.record_entry b ON (cn.record = b.id)
3987                   WHERE NOT cp.deleted
3988                         AND NOT cn.deleted
3989                         AND NOT b.deleted
3990                         AND cs.opac_visible
3991                         AND cl.opac_visible
3992                         AND cp.opac_visible
3993                         AND a.opac_visible
3994                             UNION
3995                 SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number
3996                   FROM  asset.copy cp
3997                         JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
3998                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3999                         JOIN asset.copy_location cl ON (cp.location = cl.id)
4000                         JOIN config.copy_status cs ON (cp.status = cs.id)
4001                   WHERE NOT cp.deleted
4002                         AND cs.opac_visible
4003                         AND cl.opac_visible
4004                         AND cp.opac_visible
4005                         AND a.opac_visible
4006                     ) AS x 
4007
4008     $$;
4009  
4010     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
4011
4012     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
4013         IF TG_OP = 'INSERT' THEN
4014             add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
4015             EXECUTE add_query;
4016             RETURN NEW;
4017         ELSE
4018             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
4019             EXECUTE remove_query;
4020             RETURN OLD;
4021         END IF;
4022     END IF;
4023
4024     IF TG_OP = 'INSERT' THEN
4025
4026         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
4027             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
4028             EXECUTE add_query;
4029         END IF;
4030
4031         RETURN NEW;
4032
4033     END IF;
4034
4035     -- handle items first, since with circulation activity
4036     -- their statuses change frequently
4037     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
4038
4039         IF OLD.location    <> NEW.location OR
4040            OLD.call_number <> NEW.call_number OR
4041            OLD.status      <> NEW.status OR
4042            OLD.circ_lib    <> NEW.circ_lib THEN
4043             -- any of these could change visibility, but
4044             -- we'll save some queries and not try to calculate
4045             -- the change directly
4046             do_remove := true;
4047             do_add := true;
4048         ELSE
4049
4050             IF OLD.deleted <> NEW.deleted THEN
4051                 IF NEW.deleted THEN
4052                     do_remove := true;
4053                 ELSE
4054                     do_add := true;
4055                 END IF;
4056             END IF;
4057
4058             IF OLD.opac_visible <> NEW.opac_visible THEN
4059                 IF OLD.opac_visible THEN
4060                     do_remove := true;
4061                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
4062                                         -- is also marked opac_visible
4063                     do_add := true;
4064                 END IF;
4065             END IF;
4066
4067         END IF;
4068
4069         IF do_remove THEN
4070             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
4071         END IF;
4072         IF do_add THEN
4073             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
4074             EXECUTE add_query;
4075         END IF;
4076
4077         RETURN NEW;
4078
4079     END IF;
4080
4081     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
4082  
4083         IF OLD.deleted AND NEW.deleted THEN -- do nothing
4084
4085             RETURN NEW;
4086  
4087         ELSIF NEW.deleted THEN -- remove rows
4088  
4089             IF TG_TABLE_NAME = 'call_number' THEN
4090                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
4091             ELSIF TG_TABLE_NAME = 'record_entry' THEN
4092                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
4093             END IF;
4094  
4095             RETURN NEW;
4096  
4097         ELSIF OLD.deleted THEN -- add rows
4098  
4099             IF TG_TABLE_NAME IN ('copy','unit') THEN
4100                 add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
4101             ELSIF TG_TABLE_NAME = 'call_number' THEN
4102                 add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
4103             ELSIF TG_TABLE_NAME = 'record_entry' THEN
4104                 add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
4105             END IF;
4106  
4107             EXECUTE add_query;
4108             RETURN NEW;
4109  
4110         END IF;
4111  
4112     END IF;
4113
4114     IF TG_TABLE_NAME = 'call_number' THEN
4115
4116         IF OLD.record <> NEW.record THEN
4117             -- call number is linked to different bib
4118             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
4119             EXECUTE remove_query;
4120             add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
4121             EXECUTE add_query;
4122         END IF;
4123
4124         RETURN NEW;
4125
4126     END IF;
4127
4128     IF TG_TABLE_NAME IN ('record_entry') THEN
4129         RETURN NEW; -- don't have 'opac_visible'
4130     END IF;
4131
4132     -- actor.org_unit, asset.copy_location, asset.copy_status
4133     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
4134
4135         RETURN NEW;
4136
4137     ELSIF NEW.opac_visible THEN -- add rows
4138
4139         IF TG_TABLE_NAME = 'org_unit' THEN
4140             add_query := add_query || 'AND cp.circ_lib = ' || NEW.id || ';';
4141         ELSIF TG_TABLE_NAME = 'copy_location' THEN
4142             add_query := add_query || 'AND cp.location = ' || NEW.id || ';';
4143         ELSIF TG_TABLE_NAME = 'copy_status' THEN
4144             add_query := add_query || 'AND cp.status = ' || NEW.id || ';';
4145         END IF;
4146  
4147         EXECUTE add_query;
4148  
4149     ELSE -- delete rows
4150
4151         IF TG_TABLE_NAME = 'org_unit' THEN
4152             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
4153         ELSIF TG_TABLE_NAME = 'copy_location' THEN
4154             remove_query := remove_query || 'location = ' || NEW.id || ');';
4155         ELSIF TG_TABLE_NAME = 'copy_status' THEN
4156             remove_query := remove_query || 'status = ' || NEW.id || ');';
4157         END IF;
4158  
4159         EXECUTE remove_query;
4160  
4161     END IF;
4162  
4163     RETURN NEW;
4164 END;
4165 $func$ LANGUAGE PLPGSQL;
4166 COMMENT ON FUNCTION asset.cache_copy_visibility() IS $$
4167 Trigger function to update the copy OPAC visiblity cache.
4168 $$;
4169
4170 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();
4171
4172 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
4173 DECLARE
4174     transformed_xml TEXT;
4175     prev_xfrm       TEXT;
4176     normalizer      RECORD;
4177     xfrm            config.xml_transform%ROWTYPE;
4178     attr_value      TEXT;
4179     new_attrs       HSTORE := ''::HSTORE;
4180     attr_def        config.record_attr_definition%ROWTYPE;
4181 BEGIN
4182
4183     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
4184         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
4185         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
4186         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
4187         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
4188         RETURN NEW; -- and we're done
4189     END IF;
4190
4191     IF TG_OP = 'UPDATE' THEN -- re-ingest?
4192         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
4193
4194         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
4195             RETURN NEW;
4196         END IF;
4197     END IF;
4198
4199     -- Record authority linking
4200     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
4201     IF NOT FOUND THEN
4202         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
4203     END IF;
4204
4205     -- Flatten and insert the mfr data
4206     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
4207     IF NOT FOUND THEN
4208         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
4209
4210         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
4211         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
4212         IF NOT FOUND THEN
4213             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
4214
4215                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
4216                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
4217                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
4218                       WHERE record = NEW.id
4219                             AND tag LIKE attr_def.tag
4220                             AND CASE
4221                                 WHEN attr_def.sf_list IS NOT NULL 
4222                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
4223                                 ELSE TRUE
4224                                 END
4225                       GROUP BY tag
4226                       ORDER BY tag
4227                       LIMIT 1;
4228
4229                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
4230                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
4231
4232                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
4233
4234                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
4235             
4236                     -- See if we can skip the XSLT ... it's expensive
4237                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
4238                         -- Can't skip the transform
4239                         IF xfrm.xslt <> '---' THEN
4240                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
4241                         ELSE
4242                             transformed_xml := NEW.marc;
4243                         END IF;
4244             
4245                         prev_xfrm := xfrm.name;
4246                     END IF;
4247
4248                     IF xfrm.name IS NULL THEN
4249                         -- just grab the marcxml (empty) transform
4250                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
4251                         prev_xfrm := xfrm.name;
4252                     END IF;
4253
4254                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
4255
4256                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
4257                     SELECT  value::TEXT INTO attr_value
4258                       FROM  biblio.marc21_physical_characteristics(NEW.id)
4259                       WHERE subfield = attr_def.phys_char_sf
4260                       LIMIT 1; -- Just in case ...
4261
4262                 END IF;
4263
4264                 -- apply index normalizers to attr_value
4265                 FOR normalizer IN
4266                     SELECT  n.func AS func,
4267                             n.param_count AS param_count,
4268                             m.params AS params
4269                       FROM  config.index_normalizer n
4270                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
4271                       WHERE attr = attr_def.name
4272                       ORDER BY m.pos LOOP
4273                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
4274                             quote_literal( attr_value ) ||
4275                             CASE
4276                                 WHEN normalizer.param_count > 0
4277                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
4278                                     ELSE ''
4279                                 END ||
4280                             ')' INTO attr_value;
4281         
4282                 END LOOP;
4283
4284                 -- Add the new value to the hstore
4285                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
4286
4287             END LOOP;
4288
4289             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
4290                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
4291             ELSE
4292                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
4293             END IF;
4294
4295         END IF;
4296     END IF;
4297
4298     -- Gather and insert the field entry data
4299     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
4300
4301     -- Located URI magic
4302     IF TG_OP = 'INSERT' THEN
4303         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4304         IF NOT FOUND THEN
4305             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4306         END IF;
4307     ELSE
4308         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4309         IF NOT FOUND THEN
4310             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4311         END IF;
4312     END IF;
4313
4314     -- (re)map metarecord-bib linking
4315     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
4316         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
4317         IF NOT FOUND THEN
4318             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4319         END IF;
4320     ELSE -- we're doing an update, and we're not deleted, remap
4321         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
4322         IF NOT FOUND THEN
4323             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4324         END IF;
4325     END IF;
4326
4327     RETURN NEW;
4328 END;
4329 $func$ LANGUAGE PLPGSQL;
4330
4331 -- 0513
4332 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$
4333         SELECT  XMLELEMENT(
4334                     name attributes,
4335                     XMLATTRIBUTES(
4336                         CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
4337                         'tag:open-ils.org:U2@mra/' || mra.id AS id,
4338                         'tag:open-ils.org:U2@bre/' || mra.id AS record
4339                     ),
4340                     (SELECT XMLAGG(foo.y)
4341                       FROM (SELECT XMLELEMENT(
4342                                 name field,
4343                                 XMLATTRIBUTES(
4344                                     key AS name,
4345                                     cvm.value AS "coded-value",
4346                                     rad.filter,
4347                                     rad.sorter
4348                                 ),
4349                                 x.value
4350                             )
4351                            FROM EACH(mra.attrs) AS x
4352                                 JOIN config.record_attr_definition rad ON (x.key = rad.name)
4353                                 LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = x.key AND code = x.value)
4354                         )foo(y)
4355                     )
4356                 )
4357           FROM  metabib.record_attr mra
4358           WHERE mra.id = $1;
4359 $F$ LANGUAGE SQL;
4360
4361 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$
4362 DECLARE
4363     me      biblio.record_entry%ROWTYPE;
4364     layout  unapi.bre_output_layout%ROWTYPE;
4365     xfrm    config.xml_transform%ROWTYPE;
4366     ouid    INT;
4367     tmp_xml TEXT;
4368     top_el  TEXT;
4369     output  XML;
4370     hxml    XML;
4371     axml    XML;
4372 BEGIN
4373
4374     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
4375
4376     IF ouid IS NULL THEN
4377         RETURN NULL::XML;
4378     END IF;
4379
4380     IF format = 'holdings_xml' THEN -- the special case
4381         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
4382         RETURN output;
4383     END IF;
4384
4385     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
4386
4387     IF layout.name IS NULL THEN
4388         RETURN NULL::XML;
4389     END IF;
4390
4391     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
4392
4393     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
4394
4395     -- grab SVF if we need them
4396     IF ('mra' = ANY (includes)) THEN
4397         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
4398     ELSE
4399         axml := NULL::XML;
4400     END IF;
4401
4402     -- grab hodlings if we need them
4403     IF ('holdings_xml' = ANY (includes)) THEN
4404         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
4405     ELSE
4406         hxml := NULL::XML;
4407     END IF;
4408
4409
4410     -- generate our item node
4411
4412
4413     IF format = 'marcxml' THEN
4414         tmp_xml := me.marc;
4415         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
4416            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
4417         END IF;
4418     ELSE
4419         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
4420     END IF;
4421
4422     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
4423
4424     IF axml IS NOT NULL THEN
4425         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
4426     END IF;
4427
4428     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
4429         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
4430     END IF;
4431
4432     IF ('bre.unapi' = ANY (includes)) THEN
4433         output := REGEXP_REPLACE(
4434             tmp_xml,
4435             '</' || top_el || '>(.*?)',
4436             XMLELEMENT(
4437                 name abbr,
4438                 XMLATTRIBUTES(
4439                     'http://www.w3.org/1999/xhtml' AS xmlns,
4440                     'unapi-id' AS class,
4441                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
4442                 )
4443             )::TEXT || '</' || top_el || E'>\\1'
4444         );
4445     ELSE
4446         output := tmp_xml;
4447     END IF;
4448
4449     RETURN output;
4450 END;
4451 $F$ LANGUAGE PLPGSQL;
4452
4453
4454 -- 0514
4455 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$
4456 DECLARE
4457     me      biblio.record_entry%ROWTYPE;
4458     layout  unapi.bre_output_layout%ROWTYPE;
4459     xfrm    config.xml_transform%ROWTYPE;
4460     ouid    INT;
4461     tmp_xml TEXT;
4462     top_el  TEXT;
4463     output  XML;
4464     hxml    XML;
4465     axml    XML;
4466 BEGIN
4467
4468     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
4469
4470     IF ouid IS NULL THEN
4471         RETURN NULL::XML;
4472     END IF;
4473
4474     IF format = 'holdings_xml' THEN -- the special case
4475         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
4476         RETURN output;
4477     END IF;
4478
4479     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
4480
4481     IF layout.name IS NULL THEN
4482         RETURN NULL::XML;
4483     END IF;
4484
4485     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
4486
4487     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
4488
4489     -- grab SVF if we need them
4490     IF ('mra' = ANY (includes)) THEN
4491         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
4492     ELSE
4493         axml := NULL::XML;
4494     END IF;
4495
4496     -- grab hodlings if we need them
4497     IF ('holdings_xml' = ANY (includes)) THEN
4498         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
4499     ELSE
4500         hxml := NULL::XML;
4501     END IF;
4502
4503
4504     -- generate our item node
4505
4506
4507     IF format = 'marcxml' THEN
4508         tmp_xml := me.marc;
4509         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
4510            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
4511         END IF;
4512     ELSE
4513         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
4514     END IF;
4515
4516     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
4517
4518     IF axml IS NOT NULL THEN
4519         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
4520     END IF;
4521
4522     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
4523         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
4524     END IF;
4525
4526     IF ('bre.unapi' = ANY (includes)) THEN
4527         output := REGEXP_REPLACE(
4528             tmp_xml,
4529             '</' || top_el || '>(.*?)',
4530             XMLELEMENT(
4531                 name abbr,
4532                 XMLATTRIBUTES(
4533                     'http://www.w3.org/1999/xhtml' AS xmlns,
4534                     'unapi-id' AS class,
4535                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
4536                 )
4537             )::TEXT || '</' || top_el || E'>\\1'
4538         );
4539     ELSE
4540         output := tmp_xml;
4541     END IF;
4542
4543     output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
4544     RETURN output;
4545 END;
4546 $F$ LANGUAGE PLPGSQL;
4547
4548
4549
4550 -- 0516
4551 CREATE OR REPLACE FUNCTION public.extract_acq_marc_field ( BIGINT, TEXT, TEXT) RETURNS TEXT AS $$    
4552     SELECT extract_marc_field('acq.lineitem', $1, $2, $3);
4553 $$ LANGUAGE SQL;
4554
4555 -- 0518
4556 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
4557 DECLARE
4558     rtype       TEXT;
4559     ff_pos      RECORD;
4560     tag_data    RECORD;
4561     val         TEXT;
4562 BEGIN
4563     rtype := (vandelay.marc21_record_type( marc )).code;
4564     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
4565         IF ff_pos.tag = 'ldr' THEN
4566             val := oils_xpath_string('//*[local-name()="leader"]', marc);
4567             IF val IS NOT NULL THEN
4568                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
4569                 RETURN val;
4570             END IF;
4571         ELSE 
4572             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
4573                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
4574                 RETURN val;
4575             END LOOP;
4576         END IF;
4577         val := REPEAT( ff_pos.default_val, ff_pos.length );
4578         RETURN val;
4579     END LOOP;
4580
4581     RETURN NULL;
4582 END;
4583 $func$ LANGUAGE PLPGSQL;
4584
4585
4586 -- 0519
4587 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
4588 DECLARE
4589     tag_data    TEXT;
4590     rtype       TEXT;
4591     ff_pos      RECORD;
4592     output      biblio.record_ff_map%ROWTYPE;
4593 BEGIN
4594     rtype := (vandelay.marc21_record_type( marc )).code;
4595
4596     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
4597         output.ff_name  := ff_pos.fixed_field;
4598         output.ff_value := NULL;
4599
4600         IF ff_pos.tag = 'ldr' THEN
4601             output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
4602             IF output.ff_value IS NOT NULL THEN
4603                 output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
4604                 RETURN NEXT output;
4605                 output.ff_value := NULL;
4606             END IF;
4607         ELSE
4608             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
4609                 output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
4610                 IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
4611                 RETURN NEXT output;
4612                 output.ff_value := NULL;
4613             END LOOP;
4614         END IF;
4615     
4616     END LOOP;
4617
4618     RETURN;
4619 END;
4620 $func$ LANGUAGE PLPGSQL;
4621
4622
4623 -- 0521
4624 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
4625 DECLARE
4626     uris            TEXT[];
4627     uri_xml         TEXT;
4628     uri_label       TEXT;
4629     uri_href        TEXT;
4630     uri_use         TEXT;
4631     uri_owner_list  TEXT[];
4632     uri_owner       TEXT;
4633     uri_owner_id    INT;
4634     uri_id          INT;
4635     uri_cn_id       INT;
4636     uri_map_id      INT;
4637 BEGIN
4638
4639     -- Clear any URI mappings and call numbers for this bib.
4640     -- This leads to acn / auricnm inflation, but also enables
4641     -- old acn/auricnm's to go away and for bibs to be deleted.
4642     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
4643         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
4644         DELETE FROM asset.call_number WHERE id = uri_cn_id;
4645     END LOOP;
4646
4647     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
4648     IF ARRAY_UPPER(uris,1) > 0 THEN
4649         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
4650             -- First we pull info out of the 856
4651             uri_xml     := uris[i];
4652
4653             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
4654             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()|//*[@code="u"]/text()',uri_xml))[1];
4655             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
4656             CONTINUE WHEN uri_href IS NULL OR uri_label IS NULL;
4657
4658             -- Get the distinct list of libraries wanting to use 
4659             SELECT  ARRAY_ACCUM(
4660                         DISTINCT REGEXP_REPLACE(
4661                             x,
4662                             $re$^.*?\((\w+)\).*$$re$,
4663                             E'\\1'
4664                         )
4665                     ) INTO uri_owner_list
4666               FROM  UNNEST(
4667                         oils_xpath(
4668                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
4669                             uri_xml
4670                         )
4671                     )x;
4672
4673             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
4674
4675                 -- look for a matching uri
4676                 SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
4677                 IF NOT FOUND THEN -- create one
4678                     INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
4679                     IF uri_use IS NULL THEN
4680                         SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
4681                     ELSE
4682                         SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
4683                     END IF;
4684                 END IF;
4685
4686                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
4687                     uri_owner := uri_owner_list[j];
4688
4689                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
4690                     CONTINUE WHEN NOT FOUND;
4691
4692                     -- we need a call number to link through
4693                     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;
4694                     IF NOT FOUND THEN
4695                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
4696                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
4697                         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;
4698                     END IF;
4699
4700                     -- now, link them if they're not already
4701                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
4702                     IF NOT FOUND THEN
4703                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
4704                     END IF;
4705
4706                 END LOOP;
4707
4708             END IF;
4709
4710         END LOOP;
4711     END IF;
4712
4713     RETURN;
4714 END;
4715 $func$ LANGUAGE PLPGSQL;
4716
4717
4718 -- 0522
4719 UPDATE config.org_unit_setting_type SET datatype = 'string' WHERE name = 'ui.general.button_bar';
4720
4721 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');
4722
4723 UPDATE actor.org_unit_setting SET value='"circ"' WHERE name = 'ui.general.button_bar' AND value='true';
4724
4725 UPDATE actor.org_unit_setting SET value='"none"' WHERE name = 'ui.general.button_bar' AND value='false';
4726
4727
4728 -- 0523
4729 INSERT into config.org_unit_setting_type
4730 ( name, label, description, datatype, fm_class ) VALUES
4731 ( 'cat.default_copy_status_fast',
4732   oils_i18n_gettext( 'cat.default_copy_status_fast', 'Cataloging: Default copy status (fast add)', 'coust', 'label'),
4733   oils_i18n_gettext( 'cat.default_copy_status_fast', 'Default status when a copy is created using the "Fast Add" interface.', 'coust', 'description'),
4734   'link', 'ccs'
4735 );
4736
4737 INSERT into config.org_unit_setting_type
4738 ( name, label, description, datatype, fm_class ) VALUES
4739 ( 'cat.default_copy_status_normal',
4740   oils_i18n_gettext( 'cat.default_copy_status_normal', 'Cataloging: Default copy status (normal)', 'coust', 'label'),
4741   oils_i18n_gettext( 'cat.default_copy_status_normal', 'Default status when a copy is created using the normal volume/copy creator interface.', 'coust', 'description'),
4742   'link', 'ccs'
4743 );
4744
4745 -- 0524
4746 INSERT into config.org_unit_setting_type
4747 ( name, label, description, datatype ) VALUES
4748 ( 'ui.unified_volume_copy_editor',
4749   oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'GUI: Unified Volume/Item Creator/Editor', 'coust', 'label'),
4750   oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'If true combines the Volume/Copy Creator and Item Attribute Editor in some instances.', 'coust', 'description'),
4751   'bool'
4752 );
4753
4754 -- 0525
4755 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
4756 DECLARE
4757     transformed_xml TEXT;
4758     prev_xfrm       TEXT;
4759     normalizer      RECORD;
4760     xfrm            config.xml_transform%ROWTYPE;
4761     attr_value      TEXT;
4762     new_attrs       HSTORE := ''::HSTORE;
4763     attr_def        config.record_attr_definition%ROWTYPE;
4764 BEGIN
4765
4766     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
4767         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
4768         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
4769         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
4770         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
4771         RETURN NEW; -- and we're done
4772     END IF;
4773
4774     IF TG_OP = 'UPDATE' THEN -- re-ingest?
4775         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
4776
4777         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
4778             RETURN NEW;
4779         END IF;
4780     END IF;
4781
4782     -- Record authority linking
4783     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
4784     IF NOT FOUND THEN
4785         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
4786     END IF;
4787
4788     -- Flatten and insert the mfr data
4789     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
4790     IF NOT FOUND THEN
4791         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
4792
4793         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
4794         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
4795         IF NOT FOUND THEN
4796             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
4797
4798                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
4799                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
4800                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
4801                       WHERE record = NEW.id
4802                             AND tag LIKE attr_def.tag
4803                             AND CASE
4804                                 WHEN attr_def.sf_list IS NOT NULL 
4805                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
4806                                 ELSE TRUE
4807                                 END
4808                       GROUP BY tag
4809                       ORDER BY tag
4810                       LIMIT 1;
4811
4812                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
4813                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
4814
4815                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
4816
4817                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
4818             
4819                     -- See if we can skip the XSLT ... it's expensive
4820                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
4821                         -- Can't skip the transform
4822                         IF xfrm.xslt <> '---' THEN
4823                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
4824                         ELSE
4825                             transformed_xml := NEW.marc;
4826                         END IF;
4827             
4828                         prev_xfrm := xfrm.name;
4829                     END IF;
4830
4831                     IF xfrm.name IS NULL THEN
4832                         -- just grab the marcxml (empty) transform
4833                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
4834                         prev_xfrm := xfrm.name;
4835                     END IF;
4836
4837                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
4838
4839                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
4840                     SELECT  m.value INTO attr_value
4841                       FROM  biblio.marc21_physical_characteristics(NEW.id) v
4842                             JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
4843                       WHERE v.subfield = attr_def.phys_char_sf
4844                       LIMIT 1; -- Just in case ...
4845
4846                 END IF;
4847
4848                 -- apply index normalizers to attr_value
4849                 FOR normalizer IN
4850                     SELECT  n.func AS func,
4851                             n.param_count AS param_count,
4852                             m.params AS params
4853                       FROM  config.index_normalizer n
4854                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
4855                       WHERE attr = attr_def.name
4856                       ORDER BY m.pos LOOP
4857                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
4858                             quote_literal( attr_value ) ||
4859                             CASE
4860                                 WHEN normalizer.param_count > 0
4861                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
4862                                     ELSE ''
4863                                 END ||
4864                             ')' INTO attr_value;
4865         
4866                 END LOOP;
4867
4868                 -- Add the new value to the hstore
4869                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
4870
4871             END LOOP;
4872
4873             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
4874                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
4875             ELSE
4876                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
4877             END IF;
4878
4879         END IF;
4880     END IF;
4881
4882     -- Gather and insert the field entry data
4883     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
4884
4885     -- Located URI magic
4886     IF TG_OP = 'INSERT' THEN
4887         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4888         IF NOT FOUND THEN
4889             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4890         END IF;
4891     ELSE
4892         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4893         IF NOT FOUND THEN
4894             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4895         END IF;
4896     END IF;
4897
4898     -- (re)map metarecord-bib linking
4899     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
4900         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
4901         IF NOT FOUND THEN
4902             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4903         END IF;
4904     ELSE -- we're doing an update, and we're not deleted, remap
4905         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
4906         IF NOT FOUND THEN
4907             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4908         END IF;
4909     END IF;
4910
4911     RETURN NEW;
4912 END;
4913 $func$ LANGUAGE PLPGSQL;
4914
4915 ALTER TABLE config.circ_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6,2) NOT NULL DEFAULT 0.0;
4916
4917 UPDATE config.circ_matrix_weights
4918 SET marc_bib_level = marc_vr_format;
4919
4920 ALTER TABLE config.hold_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6, 2) NOT NULL DEFAULT 0.0;
4921
4922 UPDATE config.hold_matrix_weights
4923 SET marc_bib_level = marc_vr_format;
4924
4925 ALTER TABLE config.circ_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
4926
4927 ALTER TABLE config.hold_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
4928
4929 ALTER TABLE config.circ_matrix_matchpoint ADD COLUMN marc_bib_level text;
4930
4931 ALTER TABLE config.hold_matrix_matchpoint ADD COLUMN marc_bib_level text;
4932
4933 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$
4934 DECLARE
4935     cn_object       asset.call_number%ROWTYPE;
4936     rec_descriptor  metabib.rec_descriptor%ROWTYPE;
4937     cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
4938     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
4939     weights         config.circ_matrix_weights%ROWTYPE;
4940     user_age        INTERVAL;
4941     denominator     NUMERIC(6,2);
4942     row_list        INT[];
4943     result          action.found_circ_matrix_matchpoint;
4944 BEGIN
4945     -- Assume failure
4946     result.success = false;
4947
4948     -- Fetch useful data
4949     SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
4950     SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
4951
4952     -- Pre-generate this so we only calc it once
4953     IF user_object.dob IS NOT NULL THEN
4954         SELECT INTO user_age age(user_object.dob);
4955     END IF;
4956
4957     -- Grab the closest set circ weight setting.
4958     SELECT INTO weights cw.*
4959       FROM config.weight_assoc wa
4960            JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
4961            JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
4962       WHERE active
4963       ORDER BY d.distance
4964       LIMIT 1;
4965
4966     -- No weights? Bad admin! Defaults to handle that anyway.
4967     IF weights.id IS NULL THEN
4968         weights.grp                 := 11.0;
4969         weights.org_unit            := 10.0;
4970         weights.circ_modifier       := 5.0;
4971         weights.marc_type           := 4.0;
4972         weights.marc_form           := 3.0;
4973         weights.marc_bib_level      := 2.0;
4974         weights.marc_vr_format      := 2.0;
4975         weights.copy_circ_lib       := 8.0;
4976         weights.copy_owning_lib     := 8.0;
4977         weights.user_home_ou        := 8.0;
4978         weights.ref_flag            := 1.0;
4979         weights.juvenile_flag       := 6.0;
4980         weights.is_renewal          := 7.0;
4981         weights.usr_age_lower_bound := 0.0;
4982         weights.usr_age_upper_bound := 0.0;
4983     END IF;
4984
4985     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
4986     -- If you break your org tree with funky parenting this may be wrong
4987     -- 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
4988     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
4989     WITH all_distance(distance) AS (
4990             SELECT depth AS distance FROM actor.org_unit_type
4991         UNION
4992             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
4993         )
4994     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
4995
4996     -- Loop over all the potential matchpoints
4997     FOR cur_matchpoint IN
4998         SELECT m.*
4999           FROM  config.circ_matrix_matchpoint m
5000                 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
5001                 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
5002                 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
5003                 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
5004                 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
5005           WHERE m.active
5006                 -- Permission Groups
5007              -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
5008                 -- Org Units
5009              -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
5010                 AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
5011                 AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
5012                 AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
5013                 -- Circ Type
5014                 AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
5015                 -- Static User Checks
5016                 AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
5017                 AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
5018                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
5019                 -- Static Item Checks
5020                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
5021                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
5022                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
5023                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
5024                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
5025                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
5026           ORDER BY
5027                 -- Permission Groups
5028                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
5029                 -- Org Units
5030                 CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
5031                 CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
5032                 CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
5033                 CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
5034                 -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
5035                 CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
5036                 -- Static User Checks
5037                 CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
5038                 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
5039                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
5040                 -- Static Item Checks
5041                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
5042                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
5043                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
5044                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
5045                 CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
5046                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
5047                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
5048                 m.id LOOP
5049
5050         -- Record the full matching row list
5051         row_list := row_list || cur_matchpoint.id;
5052
5053         -- No matchpoint yet?
5054         IF matchpoint.id IS NULL THEN
5055             -- Take the entire matchpoint as a starting point
5056             matchpoint := cur_matchpoint;
5057             CONTINUE; -- No need to look at this row any more.
5058         END IF;
5059
5060         -- Incomplete matchpoint?
5061         IF matchpoint.circulate IS NULL THEN
5062             matchpoint.circulate := cur_matchpoint.circulate;
5063         END IF;
5064         IF matchpoint.duration_rule IS NULL THEN
5065             matchpoint.duration_rule := cur_matchpoint.duration_rule;
5066         END IF;
5067         IF matchpoint.recurring_fine_rule IS NULL THEN
5068             matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
5069         END IF;
5070         IF matchpoint.max_fine_rule IS NULL THEN
5071             matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
5072         END IF;
5073         IF matchpoint.hard_due_date IS NULL THEN
5074             matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
5075         END IF;
5076         IF matchpoint.total_copy_hold_ratio IS NULL THEN
5077             matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
5078         END IF;
5079         IF matchpoint.available_copy_hold_ratio IS NULL THEN
5080             matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
5081         END IF;
5082         IF matchpoint.renewals IS NULL THEN
5083             matchpoint.renewals := cur_matchpoint.renewals;
5084         END IF;
5085         IF matchpoint.grace_period IS NULL THEN
5086             matchpoint.grace_period := cur_matchpoint.grace_period;
5087         END IF;
5088     END LOOP;
5089
5090     -- Check required fields
5091     IF matchpoint.circulate             IS NOT NULL AND
5092        matchpoint.duration_rule         IS NOT NULL AND
5093        matchpoint.recurring_fine_rule   IS NOT NULL AND
5094        matchpoint.max_fine_rule         IS NOT NULL THEN
5095         -- All there? We have a completed match.
5096         result.success := true;
5097     END IF;
5098
5099     -- Include the assembled matchpoint, even if it isn't complete
5100     result.matchpoint := matchpoint;
5101
5102     -- Include (for debugging) the full list of matching rows
5103     result.buildrows := row_list;
5104
5105     -- Hand the result back to caller
5106     RETURN result;
5107 END;
5108 $func$ LANGUAGE plpgsql;
5109
5110 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
5111   RETURNS integer AS
5112 $func$
5113 DECLARE
5114     requestor_object    actor.usr%ROWTYPE;
5115     user_object         actor.usr%ROWTYPE;
5116     item_object         asset.copy%ROWTYPE;
5117     item_cn_object      asset.call_number%ROWTYPE;
5118     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
5119     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
5120     weights             config.hold_matrix_weights%ROWTYPE;
5121     denominator         NUMERIC(6,2);
5122 BEGIN
5123     SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
5124     SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
5125     SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
5126     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
5127     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
5128
5129     -- The item's owner should probably be the one determining if the item is holdable
5130     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
5131     -- This flag will allow for setting it to the owning library (where the call number "lives")
5132     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
5133
5134     -- Grab the closest set circ weight setting.
5135     IF NOT FOUND THEN
5136         -- Default to circ library
5137         SELECT INTO weights hw.*
5138           FROM config.weight_assoc wa
5139                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
5140                JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
5141           WHERE active
5142           ORDER BY d.distance
5143           LIMIT 1;
5144     ELSE
5145         -- Flag is set, use owning library
5146         SELECT INTO weights hw.*
5147           FROM config.weight_assoc wa
5148                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
5149                JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
5150           WHERE active
5151           ORDER BY d.distance
5152           LIMIT 1;
5153     END IF;
5154
5155     -- No weights? Bad admin! Defaults to handle that anyway.
5156     IF weights.id IS NULL THEN
5157         weights.user_home_ou    := 5.0;
5158         weights.request_ou      := 5.0;
5159         weights.pickup_ou       := 5.0;
5160         weights.item_owning_ou  := 5.0;
5161         weights.item_circ_ou    := 5.0;
5162         weights.usr_grp         := 7.0;
5163         weights.requestor_grp   := 8.0;
5164         weights.circ_modifier   := 4.0;
5165         weights.marc_type       := 3.0;
5166         weights.marc_form       := 2.0;
5167         weights.marc_bib_level  := 1.0;
5168         weights.marc_vr_format  := 1.0;
5169         weights.juvenile_flag   := 4.0;
5170         weights.ref_flag        := 0.0;
5171     END IF;
5172
5173     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
5174     -- If you break your org tree with funky parenting this may be wrong
5175     -- 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
5176     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
5177     WITH all_distance(distance) AS (
5178             SELECT depth AS distance FROM actor.org_unit_type
5179         UNION
5180             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
5181         )
5182     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
5183
5184     -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
5185     -- This may be better implemented as part of the upgrade script?
5186     -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
5187     -- Then remove this flag, of course.
5188     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
5189
5190     IF FOUND THEN
5191         -- Note: This, to me, is REALLY hacky. I put it in anyway.
5192         -- If you can't tell, this is a single call swap on two variables.
5193         SELECT INTO user_object.profile, requestor_object.profile
5194                     requestor_object.profile, user_object.profile;
5195     END IF;
5196
5197     -- Select the winning matchpoint into the matchpoint variable for returning
5198     SELECT INTO matchpoint m.*
5199       FROM  config.hold_matrix_matchpoint m
5200             /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
5201             LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
5202             LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
5203             LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
5204             LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
5205             LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
5206             LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
5207       WHERE m.active
5208             -- Permission Groups
5209          -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
5210             AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
5211             -- Org Units
5212             AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
5213             AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
5214             AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
5215             AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
5216             AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
5217             -- Static User Checks
5218             AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
5219             -- Static Item Checks
5220             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
5221             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
5222             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
5223             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
5224             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
5225             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
5226       ORDER BY
5227             -- Permission Groups
5228             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
5229             CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
5230             -- Org Units
5231             CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
5232             CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
5233             CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
5234             CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
5235             CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
5236             -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
5237             CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
5238             -- Static Item Checks
5239             CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
5240             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
5241             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
5242             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
5243             CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
5244             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
5245             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
5246             m.id;
5247
5248     -- Return just the ID for now
5249     RETURN matchpoint.id;
5250 END;
5251 $func$ LANGUAGE 'plpgsql';
5252
5253 -- 0528
5254 CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
5255 use strict;
5256 use MARC::Record;
5257 use MARC::File::XML (BinaryEncoding => 'UTF-8');
5258 use MARC::Charset;
5259 use Encode;
5260 use Unicode::Normalize;
5261
5262 MARC::Charset->assume_unicode(1);
5263
5264 my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
5265 my $schema = $_TD->{table_schema};
5266 my $rec_id = $_TD->{new}{id};
5267
5268 # Short-circuit if maintaining control numbers per MARC21 spec is not enabled
5269 my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
5270 if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
5271     return;
5272 }
5273
5274 # Get the control number identifier from an OU setting based on $_TD->{new}{owner}
5275 my $ou_cni = 'EVRGRN';
5276
5277 my $owner;
5278 if ($schema eq 'serial') {
5279     $owner = $_TD->{new}{owning_lib};
5280 } else {
5281     # are.owner and bre.owner can be null, so fall back to the consortial setting
5282     $owner = $_TD->{new}{owner} || 1;
5283 }
5284
5285 my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
5286 if ($ous_rv->{processed}) {
5287     $ou_cni = $ous_rv->{rows}[0]->{value};
5288     $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
5289 } else {
5290     # Fall back to the shortname of the OU if there was no OU setting
5291     $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
5292     if ($ous_rv->{processed}) {
5293         $ou_cni = $ous_rv->{rows}[0]->{shortname};
5294     }
5295 }
5296
5297 my ($create, $munge) = (0, 0);
5298
5299 my @scns = $record->field('035');
5300
5301 foreach my $id_field ('001', '003') {
5302     my $spec_value;
5303     my @controls = $record->field($id_field);
5304
5305     if ($id_field eq '001') {
5306         $spec_value = $rec_id;
5307     } else {
5308         $spec_value = $ou_cni;
5309     }
5310
5311     # Create the 001/003 if none exist
5312     if (scalar(@controls) == 1) {
5313         # Only one field; check to see if we need to munge it
5314         unless (grep $_->data() eq $spec_value, @controls) {
5315             $munge = 1;
5316         }
5317     } else {
5318         # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
5319         foreach my $control (@controls) {
5320             unless ($control->data() eq $spec_value) {
5321                 $record->delete_field($control);
5322             }
5323         }
5324         $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
5325         $create = 1;
5326     }
5327 }
5328
5329 # Now, if we need to munge the 001, we will first push the existing 001/003
5330 # into the 035; but if the record did not have one (and one only) 001 and 003
5331 # to begin with, skip this process
5332 if ($munge and not $create) {
5333     my $scn = "(" . $record->field('003')->data() . ")" . $record->field('001')->data();
5334
5335     # Do not create duplicate 035 fields
5336     unless (grep $_->subfield('a') eq $scn, @scns) {
5337         $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
5338     }
5339 }
5340
5341 # Set the 001/003 and update the MARC
5342 if ($create or $munge) {
5343     $record->field('001')->data($rec_id);
5344     $record->field('003')->data($ou_cni);
5345
5346     my $xml = $record->as_xml_record();
5347     $xml =~ s/\n//sgo;
5348     $xml =~ s/^<\?xml.+\?\s*>//go;
5349     $xml =~ s/>\s+</></go;
5350     $xml =~ s/\p{Cc}//go;
5351
5352     # Embed a version of OpenILS::Application::AppUtils->entityize()
5353     # to avoid having to set PERL5LIB for PostgreSQL as well
5354
5355     # If we are going to convert non-ASCII characters to XML entities,
5356     # we had better be dealing with a UTF8 string to begin with
5357     $xml = decode_utf8($xml);
5358
5359     $xml = NFC($xml);
5360
5361     # Convert raw ampersands to entities
5362     $xml =~ s/&(?!\S+;)/&amp;/gso;
5363
5364     # Convert Unicode characters to entities
5365     $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
5366
5367     $xml =~ s/[\x00-\x1f]//go;
5368     $_TD->{new}{marc} = $xml;
5369
5370     return "MODIFY";
5371 }
5372
5373 return;
5374 $func$ LANGUAGE PLPERLU;
5375
5376 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT ) RETURNS TEXT AS $func$
5377
5378     use MARC::Record;
5379     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5380     use MARC::Charset;
5381
5382     MARC::Charset->assume_unicode(1);
5383
5384     my $xml = shift;
5385     my $r = MARC::Record->new_from_xml( $xml );
5386
5387     return undef unless ($r);
5388
5389     my $id = shift() || $r->subfield( '901' => 'c' );
5390     $id =~ s/^\s*(?:\([^)]+\))?\s*(.+)\s*?$/$1/;
5391     return undef unless ($id); # We need an ID!
5392
5393     my $tmpl = MARC::Record->new();
5394     $tmpl->encoding( 'UTF-8' );
5395
5396     my @rule_fields;
5397     for my $field ( $r->field( '1..' ) ) { # Get main entry fields from the authority record
5398
5399         my $tag = $field->tag;
5400         my $i1 = $field->indicator(1);
5401         my $i2 = $field->indicator(2);
5402         my $sf = join '', map { $_->[0] } $field->subfields;
5403         my @data = map { @$_ } $field->subfields;
5404
5405         my @replace_them;
5406
5407         # Map the authority field to bib fields it can control.
5408         if ($tag >= 100 and $tag <= 111) {       # names
5409             @replace_them = map { $tag + $_ } (0, 300, 500, 600, 700);
5410         } elsif ($tag eq '130') {                # uniform title
5411             @replace_them = qw/130 240 440 730 830/;
5412         } elsif ($tag >= 150 and $tag <= 155) {  # subjects
5413             @replace_them = ($tag + 500);
5414         } elsif ($tag >= 180 and $tag <= 185) {  # floating subdivisions
5415             @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/;
5416         } else {
5417             next;
5418         }
5419
5420         # Dummy up the bib-side data
5421         $tmpl->append_fields(
5422             map {
5423                 MARC::Field->new( $_, $i1, $i2, @data )
5424             } @replace_them
5425         );
5426
5427         # Construct some 'replace' rules
5428         push @rule_fields, map { $_ . $sf . '[0~\)' .$id . '$]' } @replace_them;
5429     }
5430
5431     # Insert the replace rules into the template
5432     $tmpl->append_fields(
5433         MARC::Field->new( '905' => ' ' => ' ' => 'r' => join(',', @rule_fields ) )
5434     );
5435
5436     $xml = $tmpl->as_xml_record;
5437     $xml =~ s/^<\?.+?\?>$//mo;
5438     $xml =~ s/\n//sgo;
5439     $xml =~ s/>\s+</></sgo;
5440
5441     return $xml;
5442
5443 $func$ LANGUAGE PLPERLU;
5444
5445 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
5446
5447     use MARC::Record;
5448     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5449     use MARC::Charset;
5450     use strict;
5451
5452     MARC::Charset->assume_unicode(1);
5453
5454     my $target_xml = shift;
5455     my $source_xml = shift;
5456     my $field_spec = shift;
5457     my $force_add = shift || 0;
5458
5459     my $target_r = MARC::Record->new_from_xml( $target_xml );
5460     my $source_r = MARC::Record->new_from_xml( $source_xml );
5461
5462     return $target_xml unless ($target_r && $source_r);
5463
5464     my @field_list = split(',', $field_spec);
5465
5466     my %fields;
5467     for my $f (@field_list) {
5468         $f =~ s/^\s*//; $f =~ s/\s*$//;
5469         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
5470             my $field = $1;
5471             $field =~ s/\s+//;
5472             my $sf = $2;
5473             $sf =~ s/\s+//;
5474             my $match = $3;
5475             $match =~ s/^\s*//; $match =~ s/\s*$//;
5476             $fields{$field} = { sf => [ split('', $sf) ] };
5477             if ($match) {
5478                 my ($msf,$mre) = split('~', $match);
5479                 if (length($msf) > 0 and length($mre) > 0) {
5480                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
5481                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
5482                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
5483                 }
5484             }
5485         }
5486     }
5487
5488     for my $f ( keys %fields) {
5489         if ( @{$fields{$f}{sf}} ) {
5490             for my $from_field ($source_r->field( $f )) {
5491                 my @tos = $target_r->field( $f );
5492                 if (!@tos) {
5493                     next if (exists($fields{$f}{match}) and !$force_add);
5494                     my @new_fields = map { $_->clone } $source_r->field( $f );
5495                     $target_r->insert_fields_ordered( @new_fields );
5496                 } else {
5497                     for my $to_field (@tos) {
5498                         if (exists($fields{$f}{match})) {
5499                             next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
5500                         }
5501                         my @new_sf = map { ($_ => $from_field->subfield($_)) } @{$fields{$f}{sf}};
5502                         $to_field->add_subfields( @new_sf );
5503                     }
5504                 }
5505             }
5506         } else {
5507             my @new_fields = map { $_->clone } $source_r->field( $f );
5508             $target_r->insert_fields_ordered( @new_fields );
5509         }
5510     }
5511
5512     $target_xml = $target_r->as_xml_record;
5513     $target_xml =~ s/^<\?.+?\?>$//mo;
5514     $target_xml =~ s/\n//sgo;
5515     $target_xml =~ s/>\s+</></sgo;
5516
5517     return $target_xml;
5518
5519 $_$ LANGUAGE PLPERLU;
5520
5521 CREATE OR REPLACE FUNCTION authority.normalize_heading( TEXT ) RETURNS TEXT AS $func$
5522     use strict;
5523     use warnings;
5524
5525     use utf8;
5526     use MARC::Record;
5527     use MARC::File::XML (BinaryEncoding => 'UTF8');
5528     use MARC::Charset;
5529     use UUID::Tiny ':std';
5530
5531     MARC::Charset->assume_unicode(1);
5532
5533     my $xml = shift() or return undef;
5534
5535     my $r;
5536
5537     # Prevent errors in XML parsing from blowing out ungracefully
5538     eval {
5539         $r = MARC::Record->new_from_xml( $xml );
5540         1;
5541     } or do {
5542        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
5543     };
5544
5545     if (!$r) {
5546        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
5547     }
5548
5549     # From http://www.loc.gov/standards/sourcelist/subject.html
5550     my $thes_code_map = {
5551         a => 'lcsh',
5552         b => 'lcshac',
5553         c => 'mesh',
5554         d => 'nal',
5555         k => 'cash',
5556         n => 'notapplicable',
5557         r => 'aat',
5558         s => 'sears',
5559         v => 'rvm',
5560     };
5561
5562     # Default to "No attempt to code" if the leader is horribly broken
5563     my $fixed_field = $r->field('008');
5564     my $thes_char = '|';
5565     if ($fixed_field) { 
5566         $thes_char = substr($fixed_field->data(), 11, 1) || '|';
5567     }
5568
5569     my $thes_code = 'UNDEFINED';
5570
5571     if ($thes_char eq 'z') {
5572         # Grab the 040 $f per http://www.loc.gov/marc/authority/ad040.html
5573         $thes_code = $r->subfield('040', 'f') || 'UNDEFINED';
5574     } elsif ($thes_code_map->{$thes_char}) {
5575         $thes_code = $thes_code_map->{$thes_char};
5576     }
5577
5578     my $auth_txt = '';
5579     my $head = $r->field('1..');
5580     if ($head) {
5581         # Concatenate all of these subfields together, prefixed by their code
5582         # to prevent collisions along the lines of "Fiction, North Carolina"
5583         foreach my $sf ($head->subfields()) {
5584             $auth_txt .= '‡' . $sf->[0] . ' ' . $sf->[1];
5585         }
5586     }
5587     
5588     if ($auth_txt) {
5589         my $stmt = spi_prepare('SELECT public.naco_normalize($1) AS norm_text', 'TEXT');
5590         my $result = spi_exec_prepared($stmt, $auth_txt);
5591         my $norm_txt = $result->{rows}[0]->{norm_text};
5592         spi_freeplan($stmt);
5593         undef($stmt);
5594         return $head->tag() . "_" . $thes_code . " " . $norm_txt;
5595     }
5596
5597     return 'NOHEADING_' . $thes_code . ' ' . create_uuid_as_string(UUID_MD5, $xml);
5598 $func$ LANGUAGE 'plperlu' IMMUTABLE;
5599
5600 CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
5601
5602     use MARC::Record;
5603     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5604     use MARC::Charset;
5605     use strict;
5606
5607     MARC::Charset->assume_unicode(1);
5608
5609     my $xml = shift;
5610     my $r = MARC::Record->new_from_xml( $xml );
5611
5612     return $xml unless ($r);
5613
5614     my $field_spec = shift;
5615     my @field_list = split(',', $field_spec);
5616
5617     my %fields;
5618     for my $f (@field_list) {
5619         $f =~ s/^\s*//; $f =~ s/\s*$//;
5620         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
5621             my $field = $1;
5622             $field =~ s/\s+//;
5623             my $sf = $2;
5624             $sf =~ s/\s+//;
5625             my $match = $3;
5626             $match =~ s/^\s*//; $match =~ s/\s*$//;
5627             $fields{$field} = { sf => [ split('', $sf) ] };
5628             if ($match) {
5629                 my ($msf,$mre) = split('~', $match);
5630                 if (length($msf) > 0 and length($mre) > 0) {
5631                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
5632                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
5633                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
5634                 }
5635             }
5636         }
5637     }
5638
5639     for my $f ( keys %fields) {
5640         for my $to_field ($r->field( $f )) {
5641             if (exists($fields{$f}{match})) {
5642                 next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
5643             }
5644
5645             if ( @{$fields{$f}{sf}} ) {
5646                 $to_field->delete_subfield(code => $fields{$f}{sf});
5647             } else {
5648                 $r->delete_field( $to_field );
5649             }
5650         }
5651     }
5652
5653     $xml = $r->as_xml_record;
5654     $xml =~ s/^<\?.+?\?>$//mo;
5655     $xml =~ s/\n//sgo;
5656     $xml =~ s/>\s+</></sgo;
5657
5658     return $xml;
5659
5660 $_$ LANGUAGE PLPERLU;
5661
5662 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
5663
5664 use MARC::Record;
5665 use MARC::File::XML (BinaryEncoding => 'UTF-8');
5666 use MARC::Charset;
5667
5668 MARC::Charset->assume_unicode(1);
5669
5670 my $xml = shift;
5671 my $r = MARC::Record->new_from_xml( $xml );
5672
5673 return_next( { tag => 'LDR', value => $r->leader } );
5674
5675 for my $f ( $r->fields ) {
5676         if ($f->is_control_field) {
5677                 return_next({ tag => $f->tag, value => $f->data });
5678         } else {
5679                 for my $s ($f->subfields) {
5680                         return_next({
5681                                 tag      => $f->tag,
5682                                 ind1     => $f->indicator(1),
5683                                 ind2     => $f->indicator(2),
5684                                 subfield => $s->[0],
5685                                 value    => $s->[1]
5686                         });
5687
5688                         if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
5689                                 my $trim = $f->indicator(2) || 0;
5690                                 return_next({
5691                                         tag      => 'tnf',
5692                                         ind1     => $f->indicator(1),
5693                                         ind2     => $f->indicator(2),
5694                                         subfield => 'a',
5695                                         value    => substr( $s->[1], $trim )
5696                                 });
5697                         }
5698                 }
5699         }
5700 }
5701
5702 return undef;
5703
5704 $func$ LANGUAGE PLPERLU;
5705
5706 CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authority.full_rec AS $func$
5707
5708 use MARC::Record;
5709 use MARC::File::XML (BinaryEncoding => 'UTF-8');
5710 use MARC::Charset;
5711
5712 MARC::Charset->assume_unicode(1);
5713
5714 my $xml = shift;
5715 my $r = MARC::Record->new_from_xml( $xml );
5716
5717 return_next( { tag => 'LDR', value => $r->leader } );
5718
5719 for my $f ( $r->fields ) {
5720     if ($f->is_control_field) {
5721         return_next({ tag => $f->tag, value => $f->data });
5722     } else {
5723         for my $s ($f->subfields) {
5724             return_next({
5725                 tag      => $f->tag,
5726                 ind1     => $f->indicator(1),
5727                 ind2     => $f->indicator(2),
5728                 subfield => $s->[0],
5729                 value    => $s->[1]
5730             });
5731
5732         }
5733     }
5734 }
5735
5736 return undef;
5737
5738 $func$ LANGUAGE PLPERLU;
5739
5740 -- 0529
5741 INSERT INTO config.org_unit_setting_type 
5742 ( name, label, description, datatype ) VALUES 
5743 ( 'circ.user_merge.delete_addresses', 
5744   'Circ:  Patron Merge Address Delete', 
5745   'Delete address(es) of subordinate user(s) in a patron merge', 
5746    'bool'
5747 );
5748
5749 INSERT INTO config.org_unit_setting_type 
5750 ( name, label, description, datatype ) VALUES 
5751 ( 'circ.user_merge.delete_cards', 
5752   'Circ: Patron Merge Barcode Delete', 
5753   'Delete barcode(s) of subordinate user(s) in a patron merge', 
5754   'bool'
5755 );
5756
5757 INSERT INTO config.org_unit_setting_type 
5758 ( name, label, description, datatype ) VALUES 
5759 ( 'circ.user_merge.deactivate_cards', 
5760   'Circ:  Patron Merge Deactivate Card', 
5761   'Mark barcode(s) of subordinate user(s) in a patron merge as inactive', 
5762   'bool'
5763 );
5764
5765 -- 0530
5766 CREATE INDEX actor_usr_day_phone_idx_numeric ON actor.usr USING BTREE 
5767     (evergreen.lowercase(REGEXP_REPLACE(day_phone, '[^0-9]', '', 'g')));
5768
5769 CREATE INDEX actor_usr_evening_phone_idx_numeric ON actor.usr USING BTREE 
5770     (evergreen.lowercase(REGEXP_REPLACE(evening_phone, '[^0-9]', '', 'g')));
5771
5772 CREATE INDEX actor_usr_other_phone_idx_numeric ON actor.usr USING BTREE 
5773     (evergreen.lowercase(REGEXP_REPLACE(other_phone, '[^0-9]', '', 'g')));
5774
5775 -- 0533
5776 CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
5777 DECLARE
5778 found char := 'N';
5779 BEGIN
5780
5781     -- If there are any renewals for this circulation, don't archive or delete
5782     -- it yet.   We'll do so later, when we archive and delete the renewals.
5783
5784     SELECT 'Y' INTO found
5785     FROM action.circulation
5786     WHERE parent_circ = OLD.id
5787     LIMIT 1;
5788
5789     IF found = 'Y' THEN
5790         RETURN NULL;  -- don't delete
5791         END IF;
5792
5793     -- Archive a copy of the old row to action.aged_circulation
5794
5795     INSERT INTO action.aged_circulation
5796         (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
5797         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
5798         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
5799         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
5800         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
5801         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
5802       SELECT
5803         id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
5804         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
5805         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
5806         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
5807         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
5808         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
5809         FROM action.all_circulation WHERE id = OLD.id;
5810
5811     RETURN OLD;
5812 END;
5813 $$ LANGUAGE 'plpgsql';
5814
5815 -- 0534
5816 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$
5817 DECLARE
5818     matchpoint_id        INT;
5819     user_object        actor.usr%ROWTYPE;
5820     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
5821     standing_penalty    config.standing_penalty%ROWTYPE;
5822     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
5823     transit_source        actor.org_unit%ROWTYPE;
5824     item_object        asset.copy%ROWTYPE;
5825     item_cn_object     asset.call_number%ROWTYPE;
5826     ou_skip              actor.org_unit_setting%ROWTYPE;
5827     result            action.matrix_test_result;
5828     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
5829     hold_count        INT;
5830     hold_transit_prox    INT;
5831     frozen_hold_count    INT;
5832     context_org_list    INT[];
5833     done            BOOL := FALSE;
5834 BEGIN
5835     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
5836     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( pickup_ou );
5837
5838     result.success := TRUE;
5839
5840     -- Fail if we couldn't find a user
5841     IF user_object.id IS NULL THEN
5842         result.fail_part := 'no_user';
5843         result.success := FALSE;
5844         done := TRUE;
5845         RETURN NEXT result;
5846         RETURN;
5847     END IF;
5848
5849     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
5850
5851     -- Fail if we couldn't find a copy
5852     IF item_object.id IS NULL THEN
5853         result.fail_part := 'no_item';
5854         result.success := FALSE;
5855         done := TRUE;
5856         RETURN NEXT result;
5857         RETURN;
5858     END IF;
5859
5860     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
5861     result.matchpoint := matchpoint_id;
5862
5863     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
5864
5865     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
5866     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
5867         result.fail_part := 'circ.holds.target_skip_me';
5868         result.success := FALSE;
5869         done := TRUE;
5870         RETURN NEXT result;
5871         RETURN;
5872     END IF;
5873
5874     -- Fail if user is barred
5875     IF user_object.barred IS TRUE THEN
5876         result.fail_part := 'actor.usr.barred';
5877         result.success := FALSE;
5878         done := TRUE;
5879         RETURN NEXT result;
5880         RETURN;
5881     END IF;
5882
5883     -- Fail if we couldn't find any matchpoint (requires a default)
5884     IF matchpoint_id IS NULL THEN
5885         result.fail_part := 'no_matchpoint';
5886         result.success := FALSE;
5887         done := TRUE;
5888         RETURN NEXT result;
5889         RETURN;
5890     END IF;
5891
5892     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
5893
5894     IF hold_test.holdable IS FALSE THEN
5895         result.fail_part := 'config.hold_matrix_test.holdable';
5896         result.success := FALSE;
5897         done := TRUE;
5898         RETURN NEXT result;
5899     END IF;
5900
5901     IF hold_test.transit_range IS NOT NULL THEN
5902         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
5903         IF hold_test.distance_is_from_owner THEN
5904             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;
5905         ELSE
5906             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
5907         END IF;
5908
5909         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
5910
5911         IF NOT FOUND THEN
5912             result.fail_part := 'transit_range';
5913             result.success := FALSE;
5914             done := TRUE;
5915             RETURN NEXT result;
5916         END IF;
5917     END IF;
5918  
5919     FOR standing_penalty IN
5920         SELECT  DISTINCT csp.*
5921           FROM  actor.usr_standing_penalty usp
5922                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
5923           WHERE usr = match_user
5924                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
5925                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
5926                 AND csp.block_list LIKE '%HOLD%' LOOP
5927
5928         result.fail_part := standing_penalty.name;
5929         result.success := FALSE;
5930         done := TRUE;
5931         RETURN NEXT result;
5932     END LOOP;
5933
5934     IF hold_test.stop_blocked_user IS TRUE THEN
5935         FOR standing_penalty IN
5936             SELECT  DISTINCT csp.*
5937               FROM  actor.usr_standing_penalty usp
5938                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
5939               WHERE usr = match_user
5940                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
5941                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
5942                     AND csp.block_list LIKE '%CIRC%' LOOP
5943     
5944             result.fail_part := standing_penalty.name;
5945             result.success := FALSE;
5946             done := TRUE;
5947             RETURN NEXT result;
5948         END LOOP;
5949     END IF;
5950
5951     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
5952         SELECT    INTO hold_count COUNT(*)
5953           FROM    action.hold_request
5954           WHERE    usr = match_user
5955             AND fulfillment_time IS NULL
5956             AND cancel_time IS NULL
5957             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
5958
5959         IF hold_count >= hold_test.max_holds THEN
5960             result.fail_part := 'config.hold_matrix_test.max_holds';
5961             result.success := FALSE;
5962             done := TRUE;
5963             RETURN NEXT result;
5964         END IF;
5965     END IF;
5966
5967     IF item_object.age_protect IS NOT NULL THEN
5968         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
5969
5970         IF item_object.create_date + age_protect_object.age > NOW() THEN
5971             IF hold_test.distance_is_from_owner THEN
5972                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
5973                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
5974             ELSE
5975                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
5976             END IF;
5977
5978             IF hold_transit_prox > age_protect_object.prox THEN
5979                 result.fail_part := 'config.rule_age_hold_protect.prox';
5980                 result.success := FALSE;
5981                 done := TRUE;
5982                 RETURN NEXT result;
5983             END IF;
5984         END IF;
5985     END IF;
5986
5987     IF NOT done THEN
5988         RETURN NEXT result;
5989     END IF;
5990
5991     RETURN;
5992 END;
5993 $func$ LANGUAGE plpgsql;
5994
5995 -- do potentially large updates last to save time if upgrader needs
5996 -- to manually tweak the upgrade script to resolve errors
5997
5998 -- 0505
5999 UPDATE metabib.facet_entry SET value = evergreen.force_unicode_normal_form(value,'NFC');
6000
6001 UPDATE asset.call_number SET id = id;
6002
6003 -- Update reporter.materialized_simple_record with normalized ISBN values
6004 -- This might not get all of them, but most ISBNs will have more than one hyphen
6005 DELETE FROM reporter.materialized_simple_record WHERE id IN (
6006     SELECT record FROM metabib.full_rec WHERE tag = '020' AND subfield IN ('a', 'z') AND value LIKE '%-%-%'
6007 );
6008
6009 INSERT INTO reporter.materialized_simple_record
6010     SELECT DISTINCT rossr.* FROM reporter.old_super_simple_record rossr INNER JOIN metabib.full_rec mfr ON mfr.record = rossr.id
6011         WHERE mfr.tag = '020' AND mfr.subfield IN ('a', 'z') AND mfr.value LIKE '%-%-%'
6012 ;
6013
6014 COMMIT;
6015
6016 DROP TRIGGER IF EXISTS mat_summary_add_tgr ON money.cash_payment;
6017 DROP TRIGGER IF EXISTS mat_summary_upd_tgr ON money.cash_payment;
6018 DROP TRIGGER IF EXISTS mat_summary_del_tgr ON money.cash_payment;
6019
6020 CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('cash_payment');
6021 CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('cash_payment');
6022 CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('cash_payment');
6023
6024 DROP TRIGGER IF EXISTS mat_summary_add_tgr ON money.check_payment;
6025 DROP TRIGGER IF EXISTS mat_summary_upd_tgr ON money.check_payment;
6026 DROP TRIGGER IF EXISTS mat_summary_del_tgr ON money.check_payment;
6027
6028 CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('check_payment');
6029 CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('check_payment');
6030 CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('check_payment');
6031