]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/version-upgrade/2.5.3-2.6.0-upgrade-db.sql
482bc8584198d998346f33b7078fbe113092a3e6
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.5.3-2.6.0-upgrade-db.sql
1 --Upgrade Script for 2.5.3 to 2.6.0
2 \set eg_version '''2.6.0'''
3 BEGIN;
4 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('2.6.0', :eg_version);
5
6 -- check whether patch can be applied
7 SELECT evergreen.upgrade_deps_block_check('0851', :eg_version);
8
9 CREATE OR REPLACE FUNCTION evergreen.maintain_901 () RETURNS TRIGGER AS $func$
10 use strict;
11 use MARC::Record;
12 use MARC::File::XML (BinaryEncoding => 'UTF-8');
13 use MARC::Charset;
14 use Encode;
15 use Unicode::Normalize;
16
17 MARC::Charset->assume_unicode(1);
18
19 my $schema = $_TD->{table_schema};
20 my $marc = MARC::Record->new_from_xml($_TD->{new}{marc});
21
22 my @old901s = $marc->field('901');
23 $marc->delete_fields(@old901s);
24
25 if ($schema eq 'biblio') {
26     my $tcn_value = $_TD->{new}{tcn_value};
27
28     # Set TCN value to record ID?
29     my $id_as_tcn = spi_exec_query("
30         SELECT enabled
31         FROM config.global_flag
32         WHERE name = 'cat.bib.use_id_for_tcn'
33     ");
34     if (($id_as_tcn->{processed}) && $id_as_tcn->{rows}[0]->{enabled} eq 't') {
35         $tcn_value = $_TD->{new}{id}; 
36         $_TD->{new}{tcn_value} = $tcn_value;
37     }
38
39     my $new_901 = MARC::Field->new("901", " ", " ",
40         "a" => $tcn_value,
41         "b" => $_TD->{new}{tcn_source},
42         "c" => $_TD->{new}{id},
43         "t" => $schema
44     );
45
46     if ($_TD->{new}{owner}) {
47         $new_901->add_subfields("o" => $_TD->{new}{owner});
48     }
49
50     if ($_TD->{new}{share_depth}) {
51         $new_901->add_subfields("d" => $_TD->{new}{share_depth});
52     }
53
54     $marc->append_fields($new_901);
55 } elsif ($schema eq 'authority') {
56     my $new_901 = MARC::Field->new("901", " ", " ",
57         "c" => $_TD->{new}{id},
58         "t" => $schema,
59     );
60     $marc->append_fields($new_901);
61 } elsif ($schema eq 'serial') {
62     my $new_901 = MARC::Field->new("901", " ", " ",
63         "c" => $_TD->{new}{id},
64         "t" => $schema,
65         "o" => $_TD->{new}{owning_lib},
66     );
67
68     if ($_TD->{new}{record}) {
69         $new_901->add_subfields("r" => $_TD->{new}{record});
70     }
71
72     $marc->append_fields($new_901);
73 } else {
74     my $new_901 = MARC::Field->new("901", " ", " ",
75         "c" => $_TD->{new}{id},
76         "t" => $schema,
77     );
78     $marc->append_fields($new_901);
79 }
80
81 my $xml = $marc->as_xml_record();
82 $xml =~ s/\n//sgo;
83 $xml =~ s/^<\?xml.+\?\s*>//go;
84 $xml =~ s/>\s+</></go;
85 $xml =~ s/\p{Cc}//go;
86
87 # Embed a version of OpenILS::Application::AppUtils->entityize()
88 # to avoid having to set PERL5LIB for PostgreSQL as well
89
90 $xml = NFC($xml);
91
92 # Convert raw ampersands to entities
93 $xml =~ s/&(?!\S+;)/&amp;/gso;
94
95 # Convert Unicode characters to entities
96 $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
97
98 $xml =~ s/[\x00-\x1f]//go;
99 $_TD->{new}{marc} = $xml;
100
101 return "MODIFY";
102 $func$ LANGUAGE PLPERLU;
103
104 CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
105 use strict;
106 use MARC::Record;
107 use MARC::File::XML (BinaryEncoding => 'UTF-8');
108 use MARC::Charset;
109 use Encode;
110 use Unicode::Normalize;
111
112 MARC::Charset->assume_unicode(1);
113
114 my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
115 my $schema = $_TD->{table_schema};
116 my $rec_id = $_TD->{new}{id};
117
118 # Short-circuit if maintaining control numbers per MARC21 spec is not enabled
119 my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
120 if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
121     return;
122 }
123
124 # Get the control number identifier from an OU setting based on $_TD->{new}{owner}
125 my $ou_cni = 'EVRGRN';
126
127 my $owner;
128 if ($schema eq 'serial') {
129     $owner = $_TD->{new}{owning_lib};
130 } else {
131     # are.owner and bre.owner can be null, so fall back to the consortial setting
132     $owner = $_TD->{new}{owner} || 1;
133 }
134
135 my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
136 if ($ous_rv->{processed}) {
137     $ou_cni = $ous_rv->{rows}[0]->{value};
138     $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
139 } else {
140     # Fall back to the shortname of the OU if there was no OU setting
141     $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
142     if ($ous_rv->{processed}) {
143         $ou_cni = $ous_rv->{rows}[0]->{shortname};
144     }
145 }
146
147 my ($create, $munge) = (0, 0);
148
149 my @scns = $record->field('035');
150
151 foreach my $id_field ('001', '003') {
152     my $spec_value;
153     my @controls = $record->field($id_field);
154
155     if ($id_field eq '001') {
156         $spec_value = $rec_id;
157     } else {
158         $spec_value = $ou_cni;
159     }
160
161     # Create the 001/003 if none exist
162     if (scalar(@controls) == 1) {
163         # Only one field; check to see if we need to munge it
164         unless (grep $_->data() eq $spec_value, @controls) {
165             $munge = 1;
166         }
167     } else {
168         # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
169         foreach my $control (@controls) {
170             $record->delete_field($control);
171         }
172         $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
173         $create = 1;
174     }
175 }
176
177 my $cn = $record->field('001')->data();
178 # Special handling of OCLC numbers, often found in records that lack 003
179 if ($cn =~ /^o(c[nm]|n)\d/) {
180     $cn =~ s/^o(c[nm]|n)0*(\d+)/$2/;
181     $record->field('003')->data('OCoLC');
182     $create = 0;
183 }
184
185 # Now, if we need to munge the 001, we will first push the existing 001/003
186 # into the 035; but if the record did not have one (and one only) 001 and 003
187 # to begin with, skip this process
188 if ($munge and not $create) {
189
190     my $scn = "(" . $record->field('003')->data() . ")" . $cn;
191
192     # Do not create duplicate 035 fields
193     unless (grep $_->subfield('a') eq $scn, @scns) {
194         $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
195     }
196 }
197
198 # Set the 001/003 and update the MARC
199 if ($create or $munge) {
200     $record->field('001')->data($rec_id);
201     $record->field('003')->data($ou_cni);
202
203     my $xml = $record->as_xml_record();
204     $xml =~ s/\n//sgo;
205     $xml =~ s/^<\?xml.+\?\s*>//go;
206     $xml =~ s/>\s+</></go;
207     $xml =~ s/\p{Cc}//go;
208
209     # Embed a version of OpenILS::Application::AppUtils->entityize()
210     # to avoid having to set PERL5LIB for PostgreSQL as well
211
212     $xml = NFC($xml);
213
214     # Convert raw ampersands to entities
215     $xml =~ s/&(?!\S+;)/&amp;/gso;
216
217     # Convert Unicode characters to entities
218     $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
219
220     $xml =~ s/[\x00-\x1f]//go;
221     $_TD->{new}{marc} = $xml;
222
223     return "MODIFY";
224 }
225
226 return;
227 $func$ LANGUAGE PLPERLU;
228
229 CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
230
231     use strict;
232     use Unicode::Normalize;
233     use Encode;
234
235     my $str = shift;
236     my $sf = shift;
237
238     # Apply NACO normalization to input string; based on
239     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
240     #
241     # Note that unlike a strict reading of the NACO normalization rules,
242     # output is returned as lowercase instead of uppercase for compatibility
243     # with previous versions of the Evergreen naco_normalize routine.
244
245     # Convert to upper-case first; even though final output will be lowercase, doing this will
246     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
247     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
248     $str = uc $str;
249
250     # remove non-filing strings
251     $str =~ s/\x{0098}.*?\x{009C}//g;
252
253     $str = NFKD($str);
254
255     # additional substitutions - 3.6.
256     $str =~ s/\x{00C6}/AE/g;
257     $str =~ s/\x{00DE}/TH/g;
258     $str =~ s/\x{0152}/OE/g;
259     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
260
261     # transformations based on Unicode category codes
262     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
263
264         if ($sf && $sf =~ /^a/o) {
265                 my $commapos = index($str, ',');
266                 if ($commapos > -1) {
267                         if ($commapos != length($str) - 1) {
268                 $str =~ s/,/\x07/; # preserve first comma
269                         }
270                 }
271         }
272
273     # since we've stripped out the control characters, we can now
274     # use a few as placeholders temporarily
275     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
276     $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;
277     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
278
279     # decimal digits
280     $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/;
281
282     # intentionally skipping step 8 of the NACO algorithm; if the string
283     # gets normalized away, that's fine.
284
285     # leading and trailing spaces
286     $str =~ s/\s+/ /g;
287     $str =~ s/^\s+//;
288     $str =~ s/\s+$//g;
289
290     return lc $str;
291 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
292
293 -- Currently, the only difference from naco_normalize is that search_normalize
294 -- turns apostrophes into spaces, while naco_normalize collapses them.
295 CREATE OR REPLACE FUNCTION public.search_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
296
297     use strict;
298     use Unicode::Normalize;
299     use Encode;
300
301     my $str = shift;
302     my $sf = shift;
303
304     # Apply NACO normalization to input string; based on
305     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
306     #
307     # Note that unlike a strict reading of the NACO normalization rules,
308     # output is returned as lowercase instead of uppercase for compatibility
309     # with previous versions of the Evergreen naco_normalize routine.
310
311     # Convert to upper-case first; even though final output will be lowercase, doing this will
312     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
313     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
314     $str = uc $str;
315
316     # remove non-filing strings
317     $str =~ s/\x{0098}.*?\x{009C}//g;
318
319     $str = NFKD($str);
320
321     # additional substitutions - 3.6.
322     $str =~ s/\x{00C6}/AE/g;
323     $str =~ s/\x{00DE}/TH/g;
324     $str =~ s/\x{0152}/OE/g;
325     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}][/DDOLl/d;
326
327     # transformations based on Unicode category codes
328     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
329
330         if ($sf && $sf =~ /^a/o) {
331                 my $commapos = index($str, ',');
332                 if ($commapos > -1) {
333                         if ($commapos != length($str) - 1) {
334                 $str =~ s/,/\x07/; # preserve first comma
335                         }
336                 }
337         }
338
339     # since we've stripped out the control characters, we can now
340     # use a few as placeholders temporarily
341     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
342     $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;
343     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
344
345     # decimal digits
346     $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/;
347
348     # intentionally skipping step 8 of the NACO algorithm; if the string
349     # gets normalized away, that's fine.
350
351     # leading and trailing spaces
352     $str =~ s/\s+/ /g;
353     $str =~ s/^\s+//;
354     $str =~ s/\s+$//g;
355
356     return lc $str;
357 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
358
359 -- Evergreen DB patch XXXX.data.prefer_external_url_OUS.sql
360 --
361 -- FIXME: insert description of change, if needed
362 --
363
364 -- check whether patch can be applied
365 SELECT evergreen.upgrade_deps_block_check('0853', :eg_version);
366
367 INSERT into config.org_unit_setting_type
368 ( name, grp, label, description, datatype, fm_class ) VALUES
369 ( 'lib.prefer_external_url', 'lib',
370   'Use external "library information URL" in copy table, if available',
371   'If set to true, the library name in the copy details section will link to the URL associated with the "Library information URL" library setting rather than the library information page generated by Evergreen.',
372   'bool', null
373 );
374
375
376 SELECT evergreen.upgrade_deps_block_check('0854', :eg_version);
377
378 INSERT INTO permission.perm_list ( id, code, description ) VALUES (
379     553,
380     'UPDATE_ORG_UNIT_SETTING.circ.min_item_price',
381     oils_i18n_gettext(
382         553,
383         'UPDATE_ORG_UNIT_SETTING.circ.min_item_price',
384         'ppl',
385         'description'
386     )
387 ), (
388         554,
389     'UPDATE_ORG_UNIT_SETTING.circ.max_item_price',
390     oils_i18n_gettext(
391         554,
392         'UPDATE_ORG_UNIT_SETTING.circ.max_item_price',
393         'ppl',
394         'description'
395     )
396 );
397
398 INSERT into config.org_unit_setting_type
399     ( name, grp, label, description, datatype, fm_class )
400 VALUES (
401     'circ.min_item_price',
402         'finance',
403     oils_i18n_gettext(
404         'circ.min_item_price',
405         'Minimum Item Price',
406         'coust', 'label'),
407     oils_i18n_gettext(
408         'circ.min_item_price',
409         'When charging for lost items, charge this amount as a minimum.',
410         'coust', 'description'),
411     'currency',
412     NULL
413 ), (
414     'circ.max_item_price',
415     'finance',
416     oils_i18n_gettext(
417         'circ.max_item_price',
418         'Maximum Item Price',
419         'coust', 'label'),
420     oils_i18n_gettext(
421         'circ.max_item_price',
422         'When charging for lost items, limit the charge to this as a maximum.',
423         'coust', 'description'),
424     'currency',
425     NULL
426 );
427
428 -- Compiled list of all changed functions and views where we went from:
429 --   array_accum() to array_agg()
430 --   array_to_string(array_agg()) to string_agg()
431
432
433 SELECT evergreen.upgrade_deps_block_check('0855', :eg_version);
434
435 -- from 000.functions.general.sql
436
437
438 -- from 002.functions.config.sql
439
440 CREATE OR REPLACE FUNCTION public.extract_marc_field ( TEXT, BIGINT, TEXT, TEXT ) RETURNS TEXT AS $$
441     SELECT regexp_replace(string_agg(output,' '),$4,'','g') FROM oils_xpath_table('id', 'marc', $1, $3, 'id='||$2)x(id INT, output TEXT);
442 $$ LANGUAGE SQL;
443
444
445 -- from 011.schema.authority.sql
446
447 CREATE OR REPLACE FUNCTION authority.axis_authority_tags(a TEXT) RETURNS INT[] AS $$
448     SELECT ARRAY_AGG(field) FROM authority.browse_axis_authority_field_map WHERE axis = $1;
449 $$ LANGUAGE SQL;
450
451 CREATE OR REPLACE FUNCTION authority.axis_authority_tags_refs(a TEXT) RETURNS INT[] AS $$
452     SELECT ARRAY_AGG(y) from (
453        SELECT  unnest(ARRAY_CAT(
454                  ARRAY[a.field],
455                  (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.field)
456              )) y
457        FROM  authority.browse_axis_authority_field_map a
458        WHERE axis = $1) x
459 $$ LANGUAGE SQL;
460
461 CREATE OR REPLACE FUNCTION authority.btag_authority_tags(btag TEXT) RETURNS INT[] AS $$
462     SELECT ARRAY_AGG(authority_field) FROM authority.control_set_bib_field WHERE tag = $1
463 $$ LANGUAGE SQL;
464
465 CREATE OR REPLACE FUNCTION authority.btag_authority_tags_refs(btag TEXT) RETURNS INT[] AS $$
466     SELECT ARRAY_AGG(y) from (
467         SELECT  unnest(ARRAY_CAT(
468                     ARRAY[a.authority_field],
469                     (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.authority_field)
470                 )) y
471       FROM  authority.control_set_bib_field a
472       WHERE a.tag = $1) x
473 $$ LANGUAGE SQL;
474
475 CREATE OR REPLACE FUNCTION authority.atag_authority_tags(atag TEXT) RETURNS INT[] AS $$
476     SELECT ARRAY_AGG(id) FROM authority.control_set_authority_field WHERE tag = $1
477 $$ LANGUAGE SQL;
478
479 CREATE OR REPLACE FUNCTION authority.atag_authority_tags_refs(atag TEXT) RETURNS INT[] AS $$
480     SELECT ARRAY_AGG(y) from (
481         SELECT  unnest(ARRAY_CAT(
482                     ARRAY[a.id],
483                     (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.id)
484                 )) y
485       FROM  authority.control_set_authority_field a
486       WHERE a.tag = $1) x
487 $$ LANGUAGE SQL;
488
489
490 -- from 012.schema.vandelay.sql
491
492 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT, attr_defs TEXT[]) RETURNS hstore AS $_$
493 DECLARE
494     transformed_xml TEXT;
495     prev_xfrm       TEXT;
496     normalizer      RECORD;
497     xfrm            config.xml_transform%ROWTYPE;
498     attr_value      TEXT;
499     new_attrs       HSTORE := ''::HSTORE;
500     attr_def        config.record_attr_definition%ROWTYPE;
501 BEGIN
502
503     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE name IN (SELECT * FROM UNNEST(attr_defs)) ORDER BY format LOOP
504
505         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
506             SELECT  STRING_AGG(x.value, COALESCE(attr_def.joiner,' ')) INTO attr_value
507               FROM  vandelay.flatten_marc(xml) AS x
508               WHERE x.tag LIKE attr_def.tag
509                     AND CASE
510                         WHEN attr_def.sf_list IS NOT NULL
511                             THEN POSITION(x.subfield IN attr_def.sf_list) > 0
512                         ELSE TRUE
513                         END
514               GROUP BY x.tag
515               ORDER BY x.tag
516               LIMIT 1;
517
518         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
519             attr_value := vandelay.marc21_extract_fixed_field(xml, attr_def.fixed_field);
520
521         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
522
523             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
524
525             -- See if we can skip the XSLT ... it's expensive
526             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
527                 -- Can't skip the transform
528                 IF xfrm.xslt <> '---' THEN
529                     transformed_xml := oils_xslt_process(xml,xfrm.xslt);
530                 ELSE
531                     transformed_xml := xml;
532                 END IF;
533
534                 prev_xfrm := xfrm.name;
535             END IF;
536
537             IF xfrm.name IS NULL THEN
538                 -- just grab the marcxml (empty) transform
539                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
540                 prev_xfrm := xfrm.name;
541             END IF;
542
543             attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
544
545         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
546             SELECT  m.value::TEXT INTO attr_value
547               FROM  vandelay.marc21_physical_characteristics(xml) v
548                     JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
549               WHERE v.subfield = attr_def.phys_char_sf
550               LIMIT 1; -- Just in case ...
551
552         END IF;
553
554         -- apply index normalizers to attr_value
555         FOR normalizer IN
556             SELECT  n.func AS func,
557                     n.param_count AS param_count,
558                     m.params AS params
559               FROM  config.index_normalizer n
560                     JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
561               WHERE attr = attr_def.name
562               ORDER BY m.pos LOOP
563                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
564                     quote_nullable( attr_value ) ||
565                     CASE
566                         WHEN normalizer.param_count > 0
567                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
568                             ELSE ''
569                         END ||
570                     ')' INTO attr_value;
571
572         END LOOP;
573
574         -- Add the new value to the hstore
575         new_attrs := new_attrs || hstore( attr_def.name, attr_value );
576
577     END LOOP;
578
579     RETURN new_attrs;
580 END;
581 $_$ LANGUAGE PLPGSQL;
582
583 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT ) RETURNS hstore AS $_$
584     SELECT vandelay.extract_rec_attrs( $1, (SELECT ARRAY_AGG(name) FROM config.record_attr_definition));
585 $_$ LANGUAGE SQL;
586
587 CREATE OR REPLACE FUNCTION vandelay.match_set_test_marcxml(
588     match_set_id INTEGER, record_xml TEXT, bucket_id INTEGER 
589 ) RETURNS SETOF vandelay.match_set_test_result AS $$
590 DECLARE
591     tags_rstore HSTORE;
592     svf_rstore  HSTORE;
593     coal        TEXT;
594     joins       TEXT;
595     query_      TEXT;
596     wq          TEXT;
597     qvalue      INTEGER;
598     rec         RECORD;
599 BEGIN
600     tags_rstore := vandelay.flatten_marc_hstore(record_xml);
601     svf_rstore := vandelay.extract_rec_attrs(record_xml);
602
603     CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
604     CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
605
606     -- generate the where clause and return that directly (into wq), and as
607     -- a side-effect, populate the _vandelay_tmp_[qj]rows tables.
608     wq := vandelay.get_expr_from_match_set(match_set_id, tags_rstore);
609
610     query_ := 'SELECT DISTINCT(record), ';
611
612     -- qrows table is for the quality bits we add to the SELECT clause
613     SELECT STRING_AGG(
614         'COALESCE(n' || q::TEXT || '.quality, 0)', ' + '
615     ) INTO coal FROM _vandelay_tmp_qrows;
616
617     -- our query string so far is the SELECT clause and the inital FROM.
618     -- no JOINs yet nor the WHERE clause
619     query_ := query_ || coal || ' AS quality ' || E'\n';
620
621     -- jrows table is for the joins we must make (and the real text conditions)
622     SELECT STRING_AGG(j, E'\n') INTO joins
623         FROM _vandelay_tmp_jrows;
624
625     -- add those joins and the where clause to our query.
626     query_ := query_ || joins || E'\n';
627
628     -- join the record bucket
629     IF bucket_id IS NOT NULL THEN
630         query_ := query_ || 'JOIN container.biblio_record_entry_bucket_item ' ||
631             'brebi ON (brebi.target_biblio_record_entry = record ' ||
632             'AND brebi.bucket = ' || bucket_id || E')\n';
633     END IF;
634
635     query_ := query_ || 'JOIN biblio.record_entry bre ON (bre.id = record) ' || 'WHERE ' || wq || ' AND not bre.deleted';
636
637     -- this will return rows of record,quality
638     FOR rec IN EXECUTE query_ USING tags_rstore, svf_rstore LOOP
639         RETURN NEXT rec;
640     END LOOP;
641
642     DROP TABLE _vandelay_tmp_qrows;
643     DROP TABLE _vandelay_tmp_jrows;
644     RETURN;
645 END;
646 $$ LANGUAGE PLPGSQL;
647
648 CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
649     record_xml TEXT
650 ) RETURNS HSTORE AS $func$
651 BEGIN
652     RETURN (SELECT
653         HSTORE(
654             ARRAY_AGG(tag || (COALESCE(subfield, ''))),
655             ARRAY_AGG(value)
656         )
657         FROM (
658             SELECT  tag, subfield, ARRAY_AGG(value)::TEXT AS value
659               FROM  (SELECT tag,
660                             subfield,
661                             CASE WHEN tag = '020' THEN -- caseless -- isbn
662                                 LOWER((REGEXP_MATCHES(value,$$^(\S{10,17})$$))[1] || '%')
663                             WHEN tag = '022' THEN -- caseless -- issn
664                                 LOWER((REGEXP_MATCHES(value,$$^(\S{4}[- ]?\S{4})$$))[1] || '%')
665                             WHEN tag = '024' THEN -- caseless -- upc (other)
666                                 LOWER(value || '%')
667                             ELSE
668                                 value
669                             END AS value
670                       FROM  vandelay.flatten_marc(record_xml)) x
671                 GROUP BY tag, subfield ORDER BY tag, subfield
672         ) subquery
673     );
674 END;
675 $func$ LANGUAGE PLPGSQL;
676
677 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set_point(
678     node vandelay.match_set_point,
679     tags_rstore HSTORE
680 ) RETURNS TEXT AS $$
681 DECLARE
682     q           TEXT;
683     i           INTEGER;
684     this_op     TEXT;
685     children    INTEGER[];
686     child       vandelay.match_set_point;
687 BEGIN
688     SELECT ARRAY_AGG(id) INTO children FROM vandelay.match_set_point
689         WHERE parent = node.id;
690
691     IF ARRAY_LENGTH(children, 1) > 0 THEN
692         this_op := vandelay._get_expr_render_one(node);
693         q := '(';
694         i := 1;
695         WHILE children[i] IS NOT NULL LOOP
696             SELECT * INTO child FROM vandelay.match_set_point
697                 WHERE id = children[i];
698             IF i > 1 THEN
699                 q := q || ' ' || this_op || ' ';
700             END IF;
701             i := i + 1;
702             q := q || vandelay.get_expr_from_match_set_point(child, tags_rstore);
703         END LOOP;
704         q := q || ')';
705         RETURN q;
706     ELSIF node.bool_op IS NULL THEN
707         PERFORM vandelay._get_expr_push_qrow(node);
708         PERFORM vandelay._get_expr_push_jrow(node, tags_rstore);
709         RETURN vandelay._get_expr_render_one(node);
710     ELSE
711         RETURN '';
712     END IF;
713 END;
714 $$  LANGUAGE PLPGSQL;
715
716
717 -- from 030.schema.metabib.sql
718
719 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
720 DECLARE
721     uris            TEXT[];
722     uri_xml         TEXT;
723     uri_label       TEXT;
724     uri_href        TEXT;
725     uri_use         TEXT;
726     uri_owner_list  TEXT[];
727     uri_owner       TEXT;
728     uri_owner_id    INT;
729     uri_id          INT;
730     uri_cn_id       INT;
731     uri_map_id      INT;
732 BEGIN
733
734     -- Clear any URI mappings and call numbers for this bib.
735     -- This leads to acn / auricnm inflation, but also enables
736     -- old acn/auricnm's to go away and for bibs to be deleted.
737     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
738         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
739         DELETE FROM asset.call_number WHERE id = uri_cn_id;
740     END LOOP;
741
742     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
743     IF ARRAY_UPPER(uris,1) > 0 THEN
744         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
745             -- First we pull info out of the 856
746             uri_xml     := uris[i];
747
748             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
749             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()',uri_xml))[1];
750             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
751
752             IF uri_label IS NULL THEN
753                 uri_label := uri_href;
754             END IF;
755             CONTINUE WHEN uri_href IS NULL;
756
757             -- Get the distinct list of libraries wanting to use 
758             SELECT  ARRAY_AGG(
759                         DISTINCT REGEXP_REPLACE(
760                             x,
761                             $re$^.*?\((\w+)\).*$$re$,
762                             E'\\1'
763                         )
764                     ) INTO uri_owner_list
765               FROM  UNNEST(
766                         oils_xpath(
767                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
768                             uri_xml
769                         )
770                     )x;
771
772             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
773
774                 -- look for a matching uri
775                 IF uri_use IS NULL THEN
776                     SELECT id INTO uri_id
777                         FROM asset.uri
778                         WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active
779                         ORDER BY id LIMIT 1;
780                     IF NOT FOUND THEN -- create one
781                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
782                         SELECT id INTO uri_id
783                             FROM asset.uri
784                             WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
785                     END IF;
786                 ELSE
787                     SELECT id INTO uri_id
788                         FROM asset.uri
789                         WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active
790                         ORDER BY id LIMIT 1;
791                     IF NOT FOUND THEN -- create one
792                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
793                         SELECT id INTO uri_id
794                             FROM asset.uri
795                             WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
796                     END IF;
797                 END IF;
798
799                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
800                     uri_owner := uri_owner_list[j];
801
802                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
803                     CONTINUE WHEN NOT FOUND;
804
805                     -- we need a call number to link through
806                     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;
807                     IF NOT FOUND THEN
808                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
809                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
810                         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;
811                     END IF;
812
813                     -- now, link them if they're not already
814                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
815                     IF NOT FOUND THEN
816                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
817                     END IF;
818
819                 END LOOP;
820
821             END IF;
822
823         END LOOP;
824     END IF;
825
826     RETURN;
827 END;
828 $func$ LANGUAGE PLPGSQL;
829
830 -- from 100.circ_matrix.sql
831
832 CREATE OR REPLACE FUNCTION actor.calculate_system_penalties( match_user INT, context_org INT ) RETURNS SETOF actor.usr_standing_penalty AS $func$
833 DECLARE
834     user_object         actor.usr%ROWTYPE;
835     new_sp_row          actor.usr_standing_penalty%ROWTYPE;
836     existing_sp_row     actor.usr_standing_penalty%ROWTYPE;
837     collections_fines   permission.grp_penalty_threshold%ROWTYPE;
838     max_fines           permission.grp_penalty_threshold%ROWTYPE;
839     max_overdue         permission.grp_penalty_threshold%ROWTYPE;
840     max_items_out       permission.grp_penalty_threshold%ROWTYPE;
841     max_lost            permission.grp_penalty_threshold%ROWTYPE;
842     max_longoverdue     permission.grp_penalty_threshold%ROWTYPE;
843     tmp_grp             INT;
844     items_overdue       INT;
845     items_out           INT;
846     items_lost          INT;
847     items_longoverdue   INT;
848     context_org_list    INT[];
849     current_fines        NUMERIC(8,2) := 0.0;
850     tmp_fines            NUMERIC(8,2);
851     tmp_groc            RECORD;
852     tmp_circ            RECORD;
853     tmp_org             actor.org_unit%ROWTYPE;
854     tmp_penalty         config.standing_penalty%ROWTYPE;
855     tmp_depth           INTEGER;
856 BEGIN
857     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
858
859     -- Max fines
860     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
861
862     -- Fail if the user has a high fine balance
863     LOOP
864         tmp_grp := user_object.profile;
865         LOOP
866             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 1 AND org_unit = tmp_org.id;
867
868             IF max_fines.threshold IS NULL THEN
869                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
870             ELSE
871                 EXIT;
872             END IF;
873
874             IF tmp_grp IS NULL THEN
875                 EXIT;
876             END IF;
877         END LOOP;
878
879         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
880             EXIT;
881         END IF;
882
883         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
884
885     END LOOP;
886
887     IF max_fines.threshold IS NOT NULL THEN
888
889         RETURN QUERY
890             SELECT  *
891               FROM  actor.usr_standing_penalty
892               WHERE usr = match_user
893                     AND org_unit = max_fines.org_unit
894                     AND (stop_date IS NULL or stop_date > NOW())
895                     AND standing_penalty = 1;
896
897         SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( max_fines.org_unit );
898
899         SELECT  SUM(f.balance_owed) INTO current_fines
900           FROM  money.materialized_billable_xact_summary f
901                 JOIN (
902                     SELECT  r.id
903                       FROM  booking.reservation r
904                       WHERE r.usr = match_user
905                             AND r.pickup_lib IN (SELECT * FROM unnest(context_org_list))
906                             AND xact_finish IS NULL
907                                 UNION ALL
908                     SELECT  g.id
909                       FROM  money.grocery g
910                       WHERE g.usr = match_user
911                             AND g.billing_location IN (SELECT * FROM unnest(context_org_list))
912                             AND xact_finish IS NULL
913                                 UNION ALL
914                     SELECT  circ.id
915                       FROM  action.circulation circ
916                       WHERE circ.usr = match_user
917                             AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
918                             AND xact_finish IS NULL ) l USING (id);
919
920         IF current_fines >= max_fines.threshold THEN
921             new_sp_row.usr := match_user;
922             new_sp_row.org_unit := max_fines.org_unit;
923             new_sp_row.standing_penalty := 1;
924             RETURN NEXT new_sp_row;
925         END IF;
926     END IF;
927
928     -- Start over for max overdue
929     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
930
931     -- Fail if the user has too many overdue items
932     LOOP
933         tmp_grp := user_object.profile;
934         LOOP
935
936             SELECT * INTO max_overdue FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 2 AND org_unit = tmp_org.id;
937
938             IF max_overdue.threshold IS NULL THEN
939                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
940             ELSE
941                 EXIT;
942             END IF;
943
944             IF tmp_grp IS NULL THEN
945                 EXIT;
946             END IF;
947         END LOOP;
948
949         IF max_overdue.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
950             EXIT;
951         END IF;
952
953         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
954
955     END LOOP;
956
957     IF max_overdue.threshold IS NOT NULL THEN
958
959         RETURN QUERY
960             SELECT  *
961               FROM  actor.usr_standing_penalty
962               WHERE usr = match_user
963                     AND org_unit = max_overdue.org_unit
964                     AND (stop_date IS NULL or stop_date > NOW())
965                     AND standing_penalty = 2;
966
967         SELECT  INTO items_overdue COUNT(*)
968           FROM  action.circulation circ
969                 JOIN  actor.org_unit_full_path( max_overdue.org_unit ) fp ON (circ.circ_lib = fp.id)
970           WHERE circ.usr = match_user
971             AND circ.checkin_time IS NULL
972             AND circ.due_date < NOW()
973             AND (circ.stop_fines = 'MAXFINES' OR circ.stop_fines IS NULL);
974
975         IF items_overdue >= max_overdue.threshold::INT THEN
976             new_sp_row.usr := match_user;
977             new_sp_row.org_unit := max_overdue.org_unit;
978             new_sp_row.standing_penalty := 2;
979             RETURN NEXT new_sp_row;
980         END IF;
981     END IF;
982
983     -- Start over for max out
984     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
985
986     -- Fail if the user has too many checked out items
987     LOOP
988         tmp_grp := user_object.profile;
989         LOOP
990             SELECT * INTO max_items_out FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 3 AND org_unit = tmp_org.id;
991
992             IF max_items_out.threshold IS NULL THEN
993                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
994             ELSE
995                 EXIT;
996             END IF;
997
998             IF tmp_grp IS NULL THEN
999                 EXIT;
1000             END IF;
1001         END LOOP;
1002
1003         IF max_items_out.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
1004             EXIT;
1005         END IF;
1006
1007         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1008
1009     END LOOP;
1010
1011
1012     -- Fail if the user has too many items checked out
1013     IF max_items_out.threshold IS NOT NULL THEN
1014
1015         RETURN QUERY
1016             SELECT  *
1017               FROM  actor.usr_standing_penalty
1018               WHERE usr = match_user
1019                     AND org_unit = max_items_out.org_unit
1020                     AND (stop_date IS NULL or stop_date > NOW())
1021                     AND standing_penalty = 3;
1022
1023         SELECT  INTO items_out COUNT(*)
1024           FROM  action.circulation circ
1025                 JOIN  actor.org_unit_full_path( max_items_out.org_unit ) fp ON (circ.circ_lib = fp.id)
1026           WHERE circ.usr = match_user
1027                 AND circ.checkin_time IS NULL
1028                 AND (circ.stop_fines IN (
1029                     SELECT 'MAXFINES'::TEXT
1030                     UNION ALL
1031                     SELECT 'LONGOVERDUE'::TEXT
1032                     UNION ALL
1033                     SELECT 'LOST'::TEXT
1034                     WHERE 'true' ILIKE
1035                     (
1036                         SELECT CASE
1037                             WHEN (SELECT value FROM actor.org_unit_ancestor_setting('circ.tally_lost', circ.circ_lib)) ILIKE 'true' THEN 'true'
1038                             ELSE 'false'
1039                         END
1040                     )
1041                     UNION ALL
1042                     SELECT 'CLAIMSRETURNED'::TEXT
1043                     WHERE 'false' ILIKE
1044                     (
1045                         SELECT CASE
1046                             WHEN (SELECT value FROM actor.org_unit_ancestor_setting('circ.do_not_tally_claims_returned', circ.circ_lib)) ILIKE 'true' THEN 'true'
1047                             ELSE 'false'
1048                         END
1049                     )
1050                     ) OR circ.stop_fines IS NULL)
1051                 AND xact_finish IS NULL;
1052
1053            IF items_out >= max_items_out.threshold::INT THEN
1054             new_sp_row.usr := match_user;
1055             new_sp_row.org_unit := max_items_out.org_unit;
1056             new_sp_row.standing_penalty := 3;
1057             RETURN NEXT new_sp_row;
1058            END IF;
1059     END IF;
1060
1061     -- Start over for max lost
1062     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1063
1064     -- Fail if the user has too many lost items
1065     LOOP
1066         tmp_grp := user_object.profile;
1067         LOOP
1068
1069             SELECT * INTO max_lost FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 5 AND org_unit = tmp_org.id;
1070
1071             IF max_lost.threshold IS NULL THEN
1072                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
1073             ELSE
1074                 EXIT;
1075             END IF;
1076
1077             IF tmp_grp IS NULL THEN
1078                 EXIT;
1079             END IF;
1080         END LOOP;
1081
1082         IF max_lost.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
1083             EXIT;
1084         END IF;
1085
1086         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1087
1088     END LOOP;
1089
1090     IF max_lost.threshold IS NOT NULL THEN
1091
1092         RETURN QUERY
1093             SELECT  *
1094             FROM  actor.usr_standing_penalty
1095             WHERE usr = match_user
1096                 AND org_unit = max_lost.org_unit
1097                 AND (stop_date IS NULL or stop_date > NOW())
1098                 AND standing_penalty = 5;
1099
1100         SELECT  INTO items_lost COUNT(*)
1101         FROM  action.circulation circ
1102             JOIN  actor.org_unit_full_path( max_lost.org_unit ) fp ON (circ.circ_lib = fp.id)
1103         WHERE circ.usr = match_user
1104             AND circ.checkin_time IS NULL
1105             AND (circ.stop_fines = 'LOST')
1106             AND xact_finish IS NULL;
1107
1108         IF items_lost >= max_lost.threshold::INT AND 0 < max_lost.threshold::INT THEN
1109             new_sp_row.usr := match_user;
1110             new_sp_row.org_unit := max_lost.org_unit;
1111             new_sp_row.standing_penalty := 5;
1112             RETURN NEXT new_sp_row;
1113         END IF;
1114     END IF;
1115
1116     -- Start over for max longoverdue
1117     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1118
1119     -- Fail if the user has too many longoverdue items
1120     LOOP
1121         tmp_grp := user_object.profile;
1122         LOOP
1123
1124             SELECT * INTO max_longoverdue 
1125                 FROM permission.grp_penalty_threshold 
1126                 WHERE grp = tmp_grp AND 
1127                     penalty = 35 AND 
1128                     org_unit = tmp_org.id;
1129
1130             IF max_longoverdue.threshold IS NULL THEN
1131                 SELECT parent INTO tmp_grp 
1132                     FROM permission.grp_tree WHERE id = tmp_grp;
1133             ELSE
1134                 EXIT;
1135             END IF;
1136
1137             IF tmp_grp IS NULL THEN
1138                 EXIT;
1139             END IF;
1140         END LOOP;
1141
1142         IF max_longoverdue.threshold IS NOT NULL 
1143                 OR tmp_org.parent_ou IS NULL THEN
1144             EXIT;
1145         END IF;
1146
1147         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1148
1149     END LOOP;
1150
1151     IF max_longoverdue.threshold IS NOT NULL THEN
1152
1153         RETURN QUERY
1154             SELECT  *
1155             FROM  actor.usr_standing_penalty
1156             WHERE usr = match_user
1157                 AND org_unit = max_longoverdue.org_unit
1158                 AND (stop_date IS NULL or stop_date > NOW())
1159                 AND standing_penalty = 35;
1160
1161         SELECT INTO items_longoverdue COUNT(*)
1162         FROM action.circulation circ
1163             JOIN actor.org_unit_full_path( max_longoverdue.org_unit ) fp 
1164                 ON (circ.circ_lib = fp.id)
1165         WHERE circ.usr = match_user
1166             AND circ.checkin_time IS NULL
1167             AND (circ.stop_fines = 'LONGOVERDUE')
1168             AND xact_finish IS NULL;
1169
1170         IF items_longoverdue >= max_longoverdue.threshold::INT 
1171                 AND 0 < max_longoverdue.threshold::INT THEN
1172             new_sp_row.usr := match_user;
1173             new_sp_row.org_unit := max_longoverdue.org_unit;
1174             new_sp_row.standing_penalty := 35;
1175             RETURN NEXT new_sp_row;
1176         END IF;
1177     END IF;
1178
1179
1180     -- Start over for collections warning
1181     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1182
1183     -- Fail if the user has a collections-level fine balance
1184     LOOP
1185         tmp_grp := user_object.profile;
1186         LOOP
1187             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 4 AND org_unit = tmp_org.id;
1188
1189             IF max_fines.threshold IS NULL THEN
1190                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
1191             ELSE
1192                 EXIT;
1193             END IF;
1194
1195             IF tmp_grp IS NULL THEN
1196                 EXIT;
1197             END IF;
1198         END LOOP;
1199
1200         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
1201             EXIT;
1202         END IF;
1203
1204         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1205
1206     END LOOP;
1207
1208     IF max_fines.threshold IS NOT NULL THEN
1209
1210         RETURN QUERY
1211             SELECT  *
1212               FROM  actor.usr_standing_penalty
1213               WHERE usr = match_user
1214                     AND org_unit = max_fines.org_unit
1215                     AND (stop_date IS NULL or stop_date > NOW())
1216                     AND standing_penalty = 4;
1217
1218         SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( max_fines.org_unit );
1219
1220         SELECT  SUM(f.balance_owed) INTO current_fines
1221           FROM  money.materialized_billable_xact_summary f
1222                 JOIN (
1223                     SELECT  r.id
1224                       FROM  booking.reservation r
1225                       WHERE r.usr = match_user
1226                             AND r.pickup_lib IN (SELECT * FROM unnest(context_org_list))
1227                             AND r.xact_finish IS NULL
1228                                 UNION ALL
1229                     SELECT  g.id
1230                       FROM  money.grocery g
1231                       WHERE g.usr = match_user
1232                             AND g.billing_location IN (SELECT * FROM unnest(context_org_list))
1233                             AND g.xact_finish IS NULL
1234                                 UNION ALL
1235                     SELECT  circ.id
1236                       FROM  action.circulation circ
1237                       WHERE circ.usr = match_user
1238                             AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
1239                             AND circ.xact_finish IS NULL ) l USING (id);
1240
1241         IF current_fines >= max_fines.threshold THEN
1242             new_sp_row.usr := match_user;
1243             new_sp_row.org_unit := max_fines.org_unit;
1244             new_sp_row.standing_penalty := 4;
1245             RETURN NEXT new_sp_row;
1246         END IF;
1247     END IF;
1248
1249     -- Start over for in collections
1250     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1251
1252     -- Remove the in-collections penalty if the user has paid down enough
1253     -- This penalty is different, because this code is not responsible for creating 
1254     -- new in-collections penalties, only for removing them
1255     LOOP
1256         tmp_grp := user_object.profile;
1257         LOOP
1258             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 30 AND org_unit = tmp_org.id;
1259
1260             IF max_fines.threshold IS NULL THEN
1261                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
1262             ELSE
1263                 EXIT;
1264             END IF;
1265
1266             IF tmp_grp IS NULL THEN
1267                 EXIT;
1268             END IF;
1269         END LOOP;
1270
1271         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
1272             EXIT;
1273         END IF;
1274
1275         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1276
1277     END LOOP;
1278
1279     IF max_fines.threshold IS NOT NULL THEN
1280
1281         SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( max_fines.org_unit );
1282
1283         -- first, see if the user had paid down to the threshold
1284         SELECT  SUM(f.balance_owed) INTO current_fines
1285           FROM  money.materialized_billable_xact_summary f
1286                 JOIN (
1287                     SELECT  r.id
1288                       FROM  booking.reservation r
1289                       WHERE r.usr = match_user
1290                             AND r.pickup_lib IN (SELECT * FROM unnest(context_org_list))
1291                             AND r.xact_finish IS NULL
1292                                 UNION ALL
1293                     SELECT  g.id
1294                       FROM  money.grocery g
1295                       WHERE g.usr = match_user
1296                             AND g.billing_location IN (SELECT * FROM unnest(context_org_list))
1297                             AND g.xact_finish IS NULL
1298                                 UNION ALL
1299                     SELECT  circ.id
1300                       FROM  action.circulation circ
1301                       WHERE circ.usr = match_user
1302                             AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
1303                             AND circ.xact_finish IS NULL ) l USING (id);
1304
1305         IF current_fines IS NULL OR current_fines <= max_fines.threshold THEN
1306             -- patron has paid down enough
1307
1308             SELECT INTO tmp_penalty * FROM config.standing_penalty WHERE id = 30;
1309
1310             IF tmp_penalty.org_depth IS NOT NULL THEN
1311
1312                 -- since this code is not responsible for applying the penalty, it can't 
1313                 -- guarantee the current context org will match the org at which the penalty 
1314                 --- was applied.  search up the org tree until we hit the configured penalty depth
1315                 SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1316                 SELECT INTO tmp_depth depth FROM actor.org_unit_type WHERE id = tmp_org.ou_type;
1317
1318                 WHILE tmp_depth >= tmp_penalty.org_depth LOOP
1319
1320                     RETURN QUERY
1321                         SELECT  *
1322                           FROM  actor.usr_standing_penalty
1323                           WHERE usr = match_user
1324                                 AND org_unit = tmp_org.id
1325                                 AND (stop_date IS NULL or stop_date > NOW())
1326                                 AND standing_penalty = 30;
1327
1328                     IF tmp_org.parent_ou IS NULL THEN
1329                         EXIT;
1330                     END IF;
1331
1332                     SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1333                     SELECT INTO tmp_depth depth FROM actor.org_unit_type WHERE id = tmp_org.ou_type;
1334                 END LOOP;
1335
1336             ELSE
1337
1338                 -- no penalty depth is defined, look for exact matches
1339
1340                 RETURN QUERY
1341                     SELECT  *
1342                       FROM  actor.usr_standing_penalty
1343                       WHERE usr = match_user
1344                             AND org_unit = max_fines.org_unit
1345                             AND (stop_date IS NULL or stop_date > NOW())
1346                             AND standing_penalty = 30;
1347             END IF;
1348     
1349         END IF;
1350
1351     END IF;
1352
1353     RETURN;
1354 END;
1355 $func$ LANGUAGE plpgsql;
1356
1357
1358 -- from 110.hold_matrix.sql
1359
1360 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$
1361 DECLARE
1362     matchpoint_id        INT;
1363     user_object        actor.usr%ROWTYPE;
1364     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
1365     standing_penalty    config.standing_penalty%ROWTYPE;
1366     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
1367     transit_source        actor.org_unit%ROWTYPE;
1368     item_object        asset.copy%ROWTYPE;
1369     item_cn_object     asset.call_number%ROWTYPE;
1370     item_status_object  config.copy_status%ROWTYPE;
1371     item_location_object    asset.copy_location%ROWTYPE;
1372     ou_skip              actor.org_unit_setting%ROWTYPE;
1373     result            action.matrix_test_result;
1374     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
1375     use_active_date   TEXT;
1376     age_protect_date  TIMESTAMP WITH TIME ZONE;
1377     hold_count        INT;
1378     hold_transit_prox    INT;
1379     frozen_hold_count    INT;
1380     context_org_list    INT[];
1381     done            BOOL := FALSE;
1382     hold_penalty TEXT;
1383 BEGIN
1384     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
1385     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( pickup_ou );
1386
1387     result.success := TRUE;
1388
1389     -- The HOLD penalty block only applies to new holds.
1390     -- The CAPTURE penalty block applies to existing holds.
1391     hold_penalty := 'HOLD';
1392     IF retargetting THEN
1393         hold_penalty := 'CAPTURE';
1394     END IF;
1395
1396     -- Fail if we couldn't find a user
1397     IF user_object.id IS NULL THEN
1398         result.fail_part := 'no_user';
1399         result.success := FALSE;
1400         done := TRUE;
1401         RETURN NEXT result;
1402         RETURN;
1403     END IF;
1404
1405     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
1406
1407     -- Fail if we couldn't find a copy
1408     IF item_object.id IS NULL THEN
1409         result.fail_part := 'no_item';
1410         result.success := FALSE;
1411         done := TRUE;
1412         RETURN NEXT result;
1413         RETURN;
1414     END IF;
1415
1416     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
1417     result.matchpoint := matchpoint_id;
1418
1419     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
1420
1421     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
1422     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
1423         result.fail_part := 'circ.holds.target_skip_me';
1424         result.success := FALSE;
1425         done := TRUE;
1426         RETURN NEXT result;
1427         RETURN;
1428     END IF;
1429
1430     -- Fail if user is barred
1431     IF user_object.barred IS TRUE THEN
1432         result.fail_part := 'actor.usr.barred';
1433         result.success := FALSE;
1434         done := TRUE;
1435         RETURN NEXT result;
1436         RETURN;
1437     END IF;
1438
1439     SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
1440     SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
1441     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
1442
1443     -- Fail if we couldn't find any matchpoint (requires a default)
1444     IF matchpoint_id IS NULL THEN
1445         result.fail_part := 'no_matchpoint';
1446         result.success := FALSE;
1447         done := TRUE;
1448         RETURN NEXT result;
1449         RETURN;
1450     END IF;
1451
1452     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
1453
1454     IF hold_test.holdable IS FALSE THEN
1455         result.fail_part := 'config.hold_matrix_test.holdable';
1456         result.success := FALSE;
1457         done := TRUE;
1458         RETURN NEXT result;
1459     END IF;
1460
1461     IF item_object.holdable IS FALSE THEN
1462         result.fail_part := 'item.holdable';
1463         result.success := FALSE;
1464         done := TRUE;
1465         RETURN NEXT result;
1466     END IF;
1467
1468     IF item_status_object.holdable IS FALSE THEN
1469         result.fail_part := 'status.holdable';
1470         result.success := FALSE;
1471         done := TRUE;
1472         RETURN NEXT result;
1473     END IF;
1474
1475     IF item_location_object.holdable IS FALSE THEN
1476         result.fail_part := 'location.holdable';
1477         result.success := FALSE;
1478         done := TRUE;
1479         RETURN NEXT result;
1480     END IF;
1481
1482     IF hold_test.transit_range IS NOT NULL THEN
1483         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
1484         IF hold_test.distance_is_from_owner THEN
1485             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;
1486         ELSE
1487             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
1488         END IF;
1489
1490         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
1491
1492         IF NOT FOUND THEN
1493             result.fail_part := 'transit_range';
1494             result.success := FALSE;
1495             done := TRUE;
1496             RETURN NEXT result;
1497         END IF;
1498     END IF;
1499  
1500     FOR standing_penalty IN
1501         SELECT  DISTINCT csp.*
1502           FROM  actor.usr_standing_penalty usp
1503                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1504           WHERE usr = match_user
1505                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1506                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1507                 AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
1508
1509         result.fail_part := standing_penalty.name;
1510         result.success := FALSE;
1511         done := TRUE;
1512         RETURN NEXT result;
1513     END LOOP;
1514
1515     IF hold_test.stop_blocked_user IS TRUE THEN
1516         FOR standing_penalty IN
1517             SELECT  DISTINCT csp.*
1518               FROM  actor.usr_standing_penalty usp
1519                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1520               WHERE usr = match_user
1521                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1522                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1523                     AND csp.block_list LIKE '%CIRC%' LOOP
1524     
1525             result.fail_part := standing_penalty.name;
1526             result.success := FALSE;
1527             done := TRUE;
1528             RETURN NEXT result;
1529         END LOOP;
1530     END IF;
1531
1532     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
1533         SELECT    INTO hold_count COUNT(*)
1534           FROM    action.hold_request
1535           WHERE    usr = match_user
1536             AND fulfillment_time IS NULL
1537             AND cancel_time IS NULL
1538             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
1539
1540         IF hold_count >= hold_test.max_holds THEN
1541             result.fail_part := 'config.hold_matrix_test.max_holds';
1542             result.success := FALSE;
1543             done := TRUE;
1544             RETURN NEXT result;
1545         END IF;
1546     END IF;
1547
1548     IF item_object.age_protect IS NOT NULL THEN
1549         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
1550         IF hold_test.distance_is_from_owner THEN
1551             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
1552         ELSE
1553             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
1554         END IF;
1555         IF use_active_date = 'true' THEN
1556             age_protect_date := COALESCE(item_object.active_date, NOW());
1557         ELSE
1558             age_protect_date := item_object.create_date;
1559         END IF;
1560         IF age_protect_date + age_protect_object.age > NOW() THEN
1561             IF hold_test.distance_is_from_owner THEN
1562                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
1563                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
1564             ELSE
1565                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
1566             END IF;
1567
1568             IF hold_transit_prox > age_protect_object.prox THEN
1569                 result.fail_part := 'config.rule_age_hold_protect.prox';
1570                 result.success := FALSE;
1571                 done := TRUE;
1572                 RETURN NEXT result;
1573             END IF;
1574         END IF;
1575     END IF;
1576
1577     IF NOT done THEN
1578         RETURN NEXT result;
1579     END IF;
1580
1581     RETURN;
1582 END;
1583 $func$ LANGUAGE plpgsql;
1584
1585
1586 -- from 300.schema.staged_search.sql
1587
1588
1589 -- from 990.schema.unapi.sql
1590
1591 CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT)
1592 RETURNS anyarray AS $$
1593     SELECT ARRAY_AGG(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2;
1594 $$ LANGUAGE SQL STABLE;
1595
1596
1597 -- from 999.functions.global.sql
1598
1599 CREATE OR REPLACE FUNCTION asset.merge_record_assets( target_record BIGINT, source_record BIGINT ) RETURNS INT AS $func$
1600 DECLARE
1601     moved_objects INT := 0;
1602     source_cn     asset.call_number%ROWTYPE;
1603     target_cn     asset.call_number%ROWTYPE;
1604     metarec       metabib.metarecord%ROWTYPE;
1605     hold          action.hold_request%ROWTYPE;
1606     ser_rec       serial.record_entry%ROWTYPE;
1607     ser_sub       serial.subscription%ROWTYPE;
1608     acq_lineitem  acq.lineitem%ROWTYPE;
1609     acq_request   acq.user_request%ROWTYPE;
1610     booking       booking.resource_type%ROWTYPE;
1611     source_part   biblio.monograph_part%ROWTYPE;
1612     target_part   biblio.monograph_part%ROWTYPE;
1613     multi_home    biblio.peer_bib_copy_map%ROWTYPE;
1614     uri_count     INT := 0;
1615     counter       INT := 0;
1616     uri_datafield TEXT;
1617     uri_text      TEXT := '';
1618 BEGIN
1619
1620     -- move any 856 entries on records that have at least one MARC-mapped URI entry
1621     SELECT  INTO uri_count COUNT(*)
1622       FROM  asset.uri_call_number_map m
1623             JOIN asset.call_number cn ON (m.call_number = cn.id)
1624       WHERE cn.record = source_record;
1625
1626     IF uri_count > 0 THEN
1627         
1628         -- This returns more nodes than you might expect:
1629         -- 7 instead of 1 for an 856 with $u $y $9
1630         SELECT  COUNT(*) INTO counter
1631           FROM  oils_xpath_table(
1632                     'id',
1633                     'marc',
1634                     'biblio.record_entry',
1635                     '//*[@tag="856"]',
1636                     'id=' || source_record
1637                 ) as t(i int,c text);
1638     
1639         FOR i IN 1 .. counter LOOP
1640             SELECT  '<datafield xmlns="http://www.loc.gov/MARC21/slim"' || 
1641                         ' tag="856"' ||
1642                         ' ind1="' || FIRST(ind1) || '"'  ||
1643                         ' ind2="' || FIRST(ind2) || '">' ||
1644                         STRING_AGG(
1645                             '<subfield code="' || subfield || '">' ||
1646                             regexp_replace(
1647                                 regexp_replace(
1648                                     regexp_replace(data,'&','&amp;','g'),
1649                                     '>', '&gt;', 'g'
1650                                 ),
1651                                 '<', '&lt;', 'g'
1652                             ) || '</subfield>', ''
1653                         ) || '</datafield>' INTO uri_datafield
1654               FROM  oils_xpath_table(
1655                         'id',
1656                         'marc',
1657                         'biblio.record_entry',
1658                         '//*[@tag="856"][position()=' || i || ']/@ind1|' ||
1659                         '//*[@tag="856"][position()=' || i || ']/@ind2|' ||
1660                         '//*[@tag="856"][position()=' || i || ']/*/@code|' ||
1661                         '//*[@tag="856"][position()=' || i || ']/*[@code]',
1662                         'id=' || source_record
1663                     ) as t(id int,ind1 text, ind2 text,subfield text,data text);
1664
1665             -- As most of the results will be NULL, protect against NULLifying
1666             -- the valid content that we do generate
1667             uri_text := uri_text || COALESCE(uri_datafield, '');
1668         END LOOP;
1669
1670         IF uri_text <> '' THEN
1671             UPDATE  biblio.record_entry
1672               SET   marc = regexp_replace(marc,'(</[^>]*record>)', uri_text || E'\\1')
1673               WHERE id = target_record;
1674         END IF;
1675
1676     END IF;
1677
1678         -- Find and move metarecords to the target record
1679         SELECT  INTO metarec *
1680           FROM  metabib.metarecord
1681           WHERE master_record = source_record;
1682
1683         IF FOUND THEN
1684                 UPDATE  metabib.metarecord
1685                   SET   master_record = target_record,
1686                         mods = NULL
1687                   WHERE id = metarec.id;
1688
1689                 moved_objects := moved_objects + 1;
1690         END IF;
1691
1692         -- Find call numbers attached to the source ...
1693         FOR source_cn IN SELECT * FROM asset.call_number WHERE record = source_record LOOP
1694
1695                 SELECT  INTO target_cn *
1696                   FROM  asset.call_number
1697                   WHERE label = source_cn.label
1698                         AND owning_lib = source_cn.owning_lib
1699                         AND record = target_record
1700                         AND NOT deleted;
1701
1702                 -- ... and if there's a conflicting one on the target ...
1703                 IF FOUND THEN
1704
1705                         -- ... move the copies to that, and ...
1706                         UPDATE  asset.copy
1707                           SET   call_number = target_cn.id
1708                           WHERE call_number = source_cn.id;
1709
1710                         -- ... move V holds to the move-target call number
1711                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_cn.id AND hold_type = 'V' LOOP
1712                 
1713                                 UPDATE  action.hold_request
1714                                   SET   target = target_cn.id
1715                                   WHERE id = hold.id;
1716                 
1717                                 moved_objects := moved_objects + 1;
1718                         END LOOP;
1719
1720                 -- ... if not ...
1721                 ELSE
1722                         -- ... just move the call number to the target record
1723                         UPDATE  asset.call_number
1724                           SET   record = target_record
1725                           WHERE id = source_cn.id;
1726                 END IF;
1727
1728                 moved_objects := moved_objects + 1;
1729         END LOOP;
1730
1731         -- Find T holds targeting the source record ...
1732         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_record AND hold_type = 'T' LOOP
1733
1734                 -- ... and move them to the target record
1735                 UPDATE  action.hold_request
1736                   SET   target = target_record
1737                   WHERE id = hold.id;
1738
1739                 moved_objects := moved_objects + 1;
1740         END LOOP;
1741
1742         -- Find serial records targeting the source record ...
1743         FOR ser_rec IN SELECT * FROM serial.record_entry WHERE record = source_record LOOP
1744                 -- ... and move them to the target record
1745                 UPDATE  serial.record_entry
1746                   SET   record = target_record
1747                   WHERE id = ser_rec.id;
1748
1749                 moved_objects := moved_objects + 1;
1750         END LOOP;
1751
1752         -- Find serial subscriptions targeting the source record ...
1753         FOR ser_sub IN SELECT * FROM serial.subscription WHERE record_entry = source_record LOOP
1754                 -- ... and move them to the target record
1755                 UPDATE  serial.subscription
1756                   SET   record_entry = target_record
1757                   WHERE id = ser_sub.id;
1758
1759                 moved_objects := moved_objects + 1;
1760         END LOOP;
1761
1762         -- Find booking resource types targeting the source record ...
1763         FOR booking IN SELECT * FROM booking.resource_type WHERE record = source_record LOOP
1764                 -- ... and move them to the target record
1765                 UPDATE  booking.resource_type
1766                   SET   record = target_record
1767                   WHERE id = booking.id;
1768
1769                 moved_objects := moved_objects + 1;
1770         END LOOP;
1771
1772         -- Find acq lineitems targeting the source record ...
1773         FOR acq_lineitem IN SELECT * FROM acq.lineitem WHERE eg_bib_id = source_record LOOP
1774                 -- ... and move them to the target record
1775                 UPDATE  acq.lineitem
1776                   SET   eg_bib_id = target_record
1777                   WHERE id = acq_lineitem.id;
1778
1779                 moved_objects := moved_objects + 1;
1780         END LOOP;
1781
1782         -- Find acq user purchase requests targeting the source record ...
1783         FOR acq_request IN SELECT * FROM acq.user_request WHERE eg_bib = source_record LOOP
1784                 -- ... and move them to the target record
1785                 UPDATE  acq.user_request
1786                   SET   eg_bib = target_record
1787                   WHERE id = acq_request.id;
1788
1789                 moved_objects := moved_objects + 1;
1790         END LOOP;
1791
1792         -- Find parts attached to the source ...
1793         FOR source_part IN SELECT * FROM biblio.monograph_part WHERE record = source_record LOOP
1794
1795                 SELECT  INTO target_part *
1796                   FROM  biblio.monograph_part
1797                   WHERE label = source_part.label
1798                         AND record = target_record;
1799
1800                 -- ... and if there's a conflicting one on the target ...
1801                 IF FOUND THEN
1802
1803                         -- ... move the copy-part maps to that, and ...
1804                         UPDATE  asset.copy_part_map
1805                           SET   part = target_part.id
1806                           WHERE part = source_part.id;
1807
1808                         -- ... move P holds to the move-target part
1809                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_part.id AND hold_type = 'P' LOOP
1810                 
1811                                 UPDATE  action.hold_request
1812                                   SET   target = target_part.id
1813                                   WHERE id = hold.id;
1814                 
1815                                 moved_objects := moved_objects + 1;
1816                         END LOOP;
1817
1818                 -- ... if not ...
1819                 ELSE
1820                         -- ... just move the part to the target record
1821                         UPDATE  biblio.monograph_part
1822                           SET   record = target_record
1823                           WHERE id = source_part.id;
1824                 END IF;
1825
1826                 moved_objects := moved_objects + 1;
1827         END LOOP;
1828
1829         -- Find multi_home items attached to the source ...
1830         FOR multi_home IN SELECT * FROM biblio.peer_bib_copy_map WHERE peer_record = source_record LOOP
1831                 -- ... and move them to the target record
1832                 UPDATE  biblio.peer_bib_copy_map
1833                   SET   peer_record = target_record
1834                   WHERE id = multi_home.id;
1835
1836                 moved_objects := moved_objects + 1;
1837         END LOOP;
1838
1839         -- And delete mappings where the item's home bib was merged with the peer bib
1840         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = (
1841                 SELECT (SELECT record FROM asset.call_number WHERE id = call_number)
1842                 FROM asset.copy WHERE id = target_copy
1843         );
1844
1845     -- Finally, "delete" the source record
1846     DELETE FROM biblio.record_entry WHERE id = source_record;
1847
1848         -- That's all, folks!
1849         RETURN moved_objects;
1850 END;
1851 $func$ LANGUAGE plpgsql;
1852
1853 -- from reporter-schema.sql
1854
1855 CREATE OR REPLACE VIEW reporter.simple_record AS
1856 SELECT  r.id,
1857         s.metarecord,
1858         r.fingerprint,
1859         r.quality,
1860         r.tcn_source,
1861         r.tcn_value,
1862         title.value AS title,
1863         uniform_title.value AS uniform_title,
1864         author.value AS author,
1865         publisher.value AS publisher,
1866         SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
1867         series_title.value AS series_title,
1868         series_statement.value AS series_statement,
1869         summary.value AS summary,
1870         ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
1871         ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
1872         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
1873         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
1874         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
1875         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
1876         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
1877         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
1878   FROM  biblio.record_entry r
1879         JOIN metabib.metarecord_source_map s ON (s.source = r.id)
1880         LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
1881         LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
1882         LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
1883         LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
1884         LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
1885         LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
1886         LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
1887         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')
1888         LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
1889         LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
1890   GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
1891
1892 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
1893 SELECT  r.id,
1894     r.fingerprint,
1895     r.quality,
1896     r.tcn_source,
1897     r.tcn_value,
1898     FIRST(title.value) AS title,
1899     FIRST(author.value) AS author,
1900     STRING_AGG(DISTINCT publisher.value, ', ') AS publisher,
1901     STRING_AGG(DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$), ', ') AS pubdate,
1902     CASE WHEN ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) = '{NULL}'
1903         THEN NULL
1904         ELSE ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') )
1905     END AS isbn,
1906     CASE WHEN ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) = '{NULL}'
1907         THEN NULL
1908         ELSE ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') )
1909     END AS issn
1910   FROM  biblio.record_entry r
1911     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
1912     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
1913     LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND (publisher.tag = '260' OR (publisher.tag = '264' AND publisher.ind2 = '1')) AND publisher.subfield = 'b')
1914     LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND (pubdate.tag = '260' OR (pubdate.tag = '264' AND pubdate.ind2 = '1')) AND pubdate.subfield = 'c')
1915     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
1916     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
1917   GROUP BY 1,2,3,4,5;
1918
1919
1920
1921 SELECT evergreen.upgrade_deps_block_check('0856', :eg_version);
1922
1923 CREATE OR REPLACE FUNCTION metabib.staged_browse(
1924     query                   TEXT,
1925     fields                  INT[],
1926     context_org             INT,
1927     context_locations       INT[],
1928     staff                   BOOL,
1929     browse_superpage_size   INT,
1930     count_up_from_zero      BOOL,   -- if false, count down from -1
1931     result_limit            INT,
1932     next_pivot_pos          INT
1933 ) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
1934 DECLARE
1935     curs                    REFCURSOR;
1936     rec                     RECORD;
1937     qpfts_query             TEXT;
1938     aqpfts_query            TEXT;
1939     afields                 INT[];
1940     bfields                 INT[];
1941     result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
1942     results_skipped         INT := 0;
1943     row_counter             INT := 0;
1944     row_number              INT;
1945     slice_start             INT;
1946     slice_end               INT;
1947     full_end                INT;
1948     all_records             BIGINT[];
1949     all_brecords             BIGINT[];
1950     all_arecords            BIGINT[];
1951     superpage_of_records    BIGINT[];
1952     superpage_size          INT;
1953 BEGIN
1954     IF count_up_from_zero THEN
1955         row_number := 0;
1956     ELSE
1957         row_number := -1;
1958     END IF;
1959
1960     OPEN curs FOR EXECUTE query;
1961
1962     LOOP
1963         FETCH curs INTO rec;
1964         IF NOT FOUND THEN
1965             IF result_row.pivot_point IS NOT NULL THEN
1966                 RETURN NEXT result_row;
1967             END IF;
1968             RETURN;
1969         END IF;
1970
1971
1972         -- Gather aggregate data based on the MBE row we're looking at now, authority axis
1973         SELECT INTO all_arecords, result_row.sees, afields
1974                 ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
1975                 STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
1976                 ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
1977
1978           FROM  metabib.browse_entry_simple_heading_map mbeshm
1979                 JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
1980                 JOIN authority.authority_linking aal ON ( ash.record = aal.source )
1981                 JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
1982                 JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
1983                     ash.atag = map.authority_field
1984                     AND map.metabib_field = ANY(fields)
1985                 )
1986           WHERE mbeshm.entry = rec.id;
1987
1988
1989         -- Gather aggregate data based on the MBE row we're looking at now, bib axis
1990         SELECT INTO all_brecords, result_row.authorities, bfields
1991                 ARRAY_AGG(DISTINCT source),
1992                 STRING_AGG(DISTINCT authority::TEXT, $$,$$),
1993                 ARRAY_AGG(DISTINCT def)
1994           FROM  metabib.browse_entry_def_map
1995           WHERE entry = rec.id
1996                 AND def = ANY(fields);
1997
1998         SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
1999
2000         result_row.sources := 0;
2001         result_row.asources := 0;
2002
2003         -- Bib-linked vis checking
2004         IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
2005
2006             full_end := ARRAY_LENGTH(all_brecords, 1);
2007             superpage_size := COALESCE(browse_superpage_size, full_end);
2008             slice_start := 1;
2009             slice_end := superpage_size;
2010
2011             WHILE result_row.sources = 0 AND slice_start <= full_end LOOP
2012                 superpage_of_records := all_brecords[slice_start:slice_end];
2013                 qpfts_query :=
2014                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
2015                     '1::INT AS rel FROM (SELECT UNNEST(' ||
2016                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
2017
2018                 -- We use search.query_parser_fts() for visibility testing.
2019                 -- We're calling it once per browse-superpage worth of records
2020                 -- out of the set of records related to a given mbe, until we've
2021                 -- either exhausted that set of records or found at least 1
2022                 -- visible record.
2023
2024                 SELECT INTO result_row.sources visible
2025                     FROM search.query_parser_fts(
2026                         context_org, NULL, qpfts_query, NULL,
2027                         context_locations, 0, NULL, NULL, FALSE, staff, FALSE
2028                     ) qpfts
2029                     WHERE qpfts.rel IS NULL;
2030
2031                 slice_start := slice_start + superpage_size;
2032                 slice_end := slice_end + superpage_size;
2033             END LOOP;
2034
2035             -- Accurate?  Well, probably.
2036             result_row.accurate := browse_superpage_size IS NULL OR
2037                 browse_superpage_size >= full_end;
2038
2039         END IF;
2040
2041         -- Authority-linked vis checking
2042         IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
2043
2044             full_end := ARRAY_LENGTH(all_arecords, 1);
2045             superpage_size := COALESCE(browse_superpage_size, full_end);
2046             slice_start := 1;
2047             slice_end := superpage_size;
2048
2049             WHILE result_row.asources = 0 AND slice_start <= full_end LOOP
2050                 superpage_of_records := all_arecords[slice_start:slice_end];
2051                 qpfts_query :=
2052                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
2053                     '1::INT AS rel FROM (SELECT UNNEST(' ||
2054                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
2055
2056                 -- We use search.query_parser_fts() for visibility testing.
2057                 -- We're calling it once per browse-superpage worth of records
2058                 -- out of the set of records related to a given mbe, via
2059                 -- authority until we've either exhausted that set of records
2060                 -- or found at least 1 visible record.
2061
2062                 SELECT INTO result_row.asources visible
2063                     FROM search.query_parser_fts(
2064                         context_org, NULL, qpfts_query, NULL,
2065                         context_locations, 0, NULL, NULL, FALSE, staff, FALSE
2066                     ) qpfts
2067                     WHERE qpfts.rel IS NULL;
2068
2069                 slice_start := slice_start + superpage_size;
2070                 slice_end := slice_end + superpage_size;
2071             END LOOP;
2072
2073
2074             -- Accurate?  Well, probably.
2075             result_row.aaccurate := browse_superpage_size IS NULL OR
2076                 browse_superpage_size >= full_end;
2077
2078         END IF;
2079
2080         IF result_row.sources > 0 OR result_row.asources > 0 THEN
2081
2082             -- The function that calls this function needs row_number in order
2083             -- to correctly order results from two different runs of this
2084             -- functions.
2085             result_row.row_number := row_number;
2086
2087             -- Now, if row_counter is still less than limit, return a row.  If
2088             -- not, but it is less than next_pivot_pos, continue on without
2089             -- returning actual result rows until we find
2090             -- that next pivot, and return it.
2091
2092             IF row_counter < result_limit THEN
2093                 result_row.browse_entry := rec.id;
2094                 result_row.value := rec.value;
2095
2096                 RETURN NEXT result_row;
2097             ELSE
2098                 result_row.browse_entry := NULL;
2099                 result_row.authorities := NULL;
2100                 result_row.fields := NULL;
2101                 result_row.value := NULL;
2102                 result_row.sources := NULL;
2103                 result_row.sees := NULL;
2104                 result_row.accurate := NULL;
2105                 result_row.aaccurate := NULL;
2106                 result_row.pivot_point := rec.id;
2107
2108                 IF row_counter >= next_pivot_pos THEN
2109                     RETURN NEXT result_row;
2110                     RETURN;
2111                 END IF;
2112             END IF;
2113
2114             IF count_up_from_zero THEN
2115                 row_number := row_number + 1;
2116             ELSE
2117                 row_number := row_number - 1;
2118             END IF;
2119
2120             -- row_counter is different from row_number.
2121             -- It simply counts up from zero so that we know when
2122             -- we've reached our limit.
2123             row_counter := row_counter + 1;
2124         END IF;
2125     END LOOP;
2126 END;
2127 $p$ LANGUAGE PLPGSQL;
2128
2129
2130 /*
2131  * Copyright (C) 2014  Equinox Software, Inc.
2132  * Mike Rylander <miker@esilibrary.com>
2133  *
2134  * This program is free software; you can redistribute it and/or
2135  * modify it under the terms of the GNU General Public License
2136  * as published by the Free Software Foundation; either version 2
2137  * of the License, or (at your option) any later version.
2138  *
2139  * This program is distributed in the hope that it will be useful,
2140  * but WITHOUT ANY WARRANTY; without even the implied warranty of
2141  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
2142  * GNU General Public License for more details.
2143  *
2144  */
2145
2146
2147
2148 SELECT evergreen.upgrade_deps_block_check('0857', :eg_version);
2149
2150 INSERT INTO config.global_flag (name, enabled, label)
2151 VALUES (
2152     'opac.located_uri.act_as_copy',
2153     FALSE,
2154     oils_i18n_gettext(
2155         'opac.located_uri.act_as_copy',
2156         'When enabled, Located URIs will provide visiblity behavior identical to copies.',
2157         'cgf',
2158         'label'
2159     )
2160 );
2161
2162 CREATE OR REPLACE FUNCTION search.query_parser_fts (
2163
2164     param_search_ou INT,
2165     param_depth     INT,
2166     param_query     TEXT,
2167     param_statuses  INT[],
2168     param_locations INT[],
2169     param_offset    INT,
2170     param_check     INT,
2171     param_limit     INT,
2172     metarecord      BOOL,
2173     staff           BOOL,
2174     deleted_search  BOOL,
2175     param_pref_ou   INT DEFAULT NULL
2176 ) RETURNS SETOF search.search_result AS $func$
2177 DECLARE
2178
2179     current_res         search.search_result%ROWTYPE;
2180     search_org_list     INT[];
2181     luri_org_list       INT[];
2182     tmp_int_list        INT[];
2183
2184     check_limit         INT;
2185     core_limit          INT;
2186     core_offset         INT;
2187     tmp_int             INT;
2188
2189     core_result         RECORD;
2190     core_cursor         REFCURSOR;
2191     core_rel_query      TEXT;
2192
2193     total_count         INT := 0;
2194     check_count         INT := 0;
2195     deleted_count       INT := 0;
2196     visible_count       INT := 0;
2197     excluded_count      INT := 0;
2198
2199     luri_as_copy        BOOL;
2200 BEGIN
2201
2202     check_limit := COALESCE( param_check, 1000 );
2203     core_limit  := COALESCE( param_limit, 25000 );
2204     core_offset := COALESCE( param_offset, 0 );
2205
2206     SELECT COALESCE( enabled, FALSE ) INTO luri_as_copy FROM config.global_flag WHERE name = 'opac.located_uri.act_as_copy';
2207
2208     -- core_skip_chk := COALESCE( param_skip_chk, 1 );
2209
2210     IF param_search_ou > 0 THEN
2211         IF param_depth IS NOT NULL THEN
2212             SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
2213         ELSE
2214             SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
2215         END IF;
2216
2217         IF luri_as_copy THEN
2218             SELECT ARRAY_AGG(distinct id) INTO luri_org_list FROM actor.org_unit_full_path( param_search_ou );
2219         ELSE
2220             SELECT ARRAY_AGG(distinct id) INTO luri_org_list FROM actor.org_unit_ancestors( param_search_ou );
2221         END IF;
2222
2223     ELSIF param_search_ou < 0 THEN
2224         SELECT ARRAY_AGG(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
2225
2226         FOR tmp_int IN SELECT * FROM UNNEST(search_org_list) LOOP
2227
2228             IF luri_as_copy THEN
2229                 SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_full_path( tmp_int );
2230             ELSE
2231                 SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( tmp_int );
2232             END IF;
2233
2234             luri_org_list := luri_org_list || tmp_int_list;
2235         END LOOP;
2236
2237         SELECT ARRAY_AGG(DISTINCT x.id) INTO luri_org_list FROM UNNEST(luri_org_list) x(id);
2238
2239     ELSIF param_search_ou = 0 THEN
2240         -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
2241     END IF;
2242
2243     IF param_pref_ou IS NOT NULL THEN
2244             IF luri_as_copy THEN
2245                 SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_full_path( param_pref_ou );
2246             ELSE
2247                 SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( param_pref_ou );
2248             END IF;
2249
2250         luri_org_list := luri_org_list || tmp_int_list;
2251     END IF;
2252
2253     OPEN core_cursor FOR EXECUTE param_query;
2254
2255     LOOP
2256
2257         FETCH core_cursor INTO core_result;
2258         EXIT WHEN NOT FOUND;
2259         EXIT WHEN total_count >= core_limit;
2260
2261         total_count := total_count + 1;
2262
2263         CONTINUE WHEN total_count NOT BETWEEN  core_offset + 1 AND check_limit + core_offset;
2264
2265         check_count := check_count + 1;
2266
2267         IF NOT deleted_search THEN
2268
2269             PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
2270             IF NOT FOUND THEN
2271                 -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
2272                 deleted_count := deleted_count + 1;
2273                 CONTINUE;
2274             END IF;
2275
2276             PERFORM 1
2277               FROM  biblio.record_entry b
2278                     JOIN config.bib_source s ON (b.source = s.id)
2279               WHERE s.transcendant
2280                     AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
2281
2282             IF FOUND THEN
2283                 -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
2284                 visible_count := visible_count + 1;
2285
2286                 current_res.id = core_result.id;
2287                 current_res.rel = core_result.rel;
2288
2289                 tmp_int := 1;
2290                 IF metarecord THEN
2291                     SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
2292                 END IF;
2293
2294                 IF tmp_int = 1 THEN
2295                     current_res.record = core_result.records[1];
2296                 ELSE
2297                     current_res.record = NULL;
2298                 END IF;
2299
2300                 RETURN NEXT current_res;
2301
2302                 CONTINUE;
2303             END IF;
2304
2305             PERFORM 1
2306               FROM  asset.call_number cn
2307                     JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
2308                     JOIN asset.uri uri ON (map.uri = uri.id)
2309               WHERE NOT cn.deleted
2310                     AND cn.label = '##URI##'
2311                     AND uri.active
2312                     AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
2313                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2314                     AND cn.owning_lib IN ( SELECT * FROM unnest( luri_org_list ) )
2315               LIMIT 1;
2316
2317             IF FOUND THEN
2318                 -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
2319                 visible_count := visible_count + 1;
2320
2321                 current_res.id = core_result.id;
2322                 current_res.rel = core_result.rel;
2323
2324                 tmp_int := 1;
2325                 IF metarecord THEN
2326                     SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
2327                 END IF;
2328
2329                 IF tmp_int = 1 THEN
2330                     current_res.record = core_result.records[1];
2331                 ELSE
2332                     current_res.record = NULL;
2333                 END IF;
2334
2335                 RETURN NEXT current_res;
2336
2337                 CONTINUE;
2338             END IF;
2339
2340             IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
2341
2342                 PERFORM 1
2343                   FROM  asset.call_number cn
2344                         JOIN asset.copy cp ON (cp.call_number = cn.id)
2345                   WHERE NOT cn.deleted
2346                         AND NOT cp.deleted
2347                         AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
2348                         AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2349                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2350                   LIMIT 1;
2351
2352                 IF NOT FOUND THEN
2353                     PERFORM 1
2354                       FROM  biblio.peer_bib_copy_map pr
2355                             JOIN asset.copy cp ON (cp.id = pr.target_copy)
2356                       WHERE NOT cp.deleted
2357                             AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
2358                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
2359                             AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2360                       LIMIT 1;
2361
2362                     IF NOT FOUND THEN
2363                     -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
2364                         excluded_count := excluded_count + 1;
2365                         CONTINUE;
2366                     END IF;
2367                 END IF;
2368
2369             END IF;
2370
2371             IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
2372
2373                 PERFORM 1
2374                   FROM  asset.call_number cn
2375                         JOIN asset.copy cp ON (cp.call_number = cn.id)
2376                   WHERE NOT cn.deleted
2377                         AND NOT cp.deleted
2378                         AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
2379                         AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2380                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2381                   LIMIT 1;
2382
2383                 IF NOT FOUND THEN
2384                     PERFORM 1
2385                       FROM  biblio.peer_bib_copy_map pr
2386                             JOIN asset.copy cp ON (cp.id = pr.target_copy)
2387                       WHERE NOT cp.deleted
2388                             AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
2389                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
2390                             AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2391                       LIMIT 1;
2392
2393                     IF NOT FOUND THEN
2394                         -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
2395                         excluded_count := excluded_count + 1;
2396                         CONTINUE;
2397                     END IF;
2398                 END IF;
2399
2400             END IF;
2401
2402             IF staff IS NULL OR NOT staff THEN
2403
2404                 PERFORM 1
2405                   FROM  asset.opac_visible_copies
2406                   WHERE circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2407                         AND record IN ( SELECT * FROM unnest( core_result.records ) )
2408                   LIMIT 1;
2409
2410                 IF NOT FOUND THEN
2411                     PERFORM 1
2412                       FROM  biblio.peer_bib_copy_map pr
2413                             JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
2414                       WHERE cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2415                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
2416                       LIMIT 1;
2417
2418                     IF NOT FOUND THEN
2419
2420                         -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
2421                         excluded_count := excluded_count + 1;
2422                         CONTINUE;
2423                     END IF;
2424                 END IF;
2425
2426             ELSE
2427
2428                 PERFORM 1
2429                   FROM  asset.call_number cn
2430                         JOIN asset.copy cp ON (cp.call_number = cn.id)
2431                   WHERE NOT cn.deleted
2432                         AND NOT cp.deleted
2433                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2434                         AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2435                   LIMIT 1;
2436
2437                 IF NOT FOUND THEN
2438
2439                     PERFORM 1
2440                       FROM  biblio.peer_bib_copy_map pr
2441                             JOIN asset.copy cp ON (cp.id = pr.target_copy)
2442                       WHERE NOT cp.deleted
2443                             AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2444                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
2445                       LIMIT 1;
2446
2447                     IF NOT FOUND THEN
2448
2449                         PERFORM 1
2450                           FROM  asset.call_number cn
2451                                 JOIN asset.copy cp ON (cp.call_number = cn.id)
2452                           WHERE cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2453                                 AND NOT cp.deleted
2454                           LIMIT 1;
2455
2456                         IF FOUND THEN
2457                             -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
2458                             excluded_count := excluded_count + 1;
2459                             CONTINUE;
2460                         END IF;
2461                     END IF;
2462
2463                 END IF;
2464
2465             END IF;
2466
2467         END IF;
2468
2469         visible_count := visible_count + 1;
2470
2471         current_res.id = core_result.id;
2472         current_res.rel = core_result.rel;
2473
2474         tmp_int := 1;
2475         IF metarecord THEN
2476             SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
2477         END IF;
2478
2479         IF tmp_int = 1 THEN
2480             current_res.record = core_result.records[1];
2481         ELSE
2482             current_res.record = NULL;
2483         END IF;
2484
2485         RETURN NEXT current_res;
2486
2487         IF visible_count % 1000 = 0 THEN
2488             -- RAISE NOTICE ' % visible so far ... ', visible_count;
2489         END IF;
2490
2491     END LOOP;
2492
2493     current_res.id = NULL;
2494     current_res.rel = NULL;
2495     current_res.record = NULL;
2496     current_res.total = total_count;
2497     current_res.checked = check_count;
2498     current_res.deleted = deleted_count;
2499     current_res.visible = visible_count;
2500     current_res.excluded = excluded_count;
2501
2502     CLOSE core_cursor;
2503
2504     RETURN NEXT current_res;
2505
2506 END;
2507 $func$ LANGUAGE PLPGSQL;
2508
2509 CREATE OR REPLACE FUNCTION unapi.holdings_xml (
2510     bid BIGINT,
2511     ouid INT,
2512     org TEXT,
2513     depth INT DEFAULT NULL,
2514     includes TEXT[] DEFAULT NULL::TEXT[],
2515     slimit HSTORE DEFAULT NULL,
2516     soffset HSTORE DEFAULT NULL,
2517     include_xmlns BOOL DEFAULT TRUE,
2518     pref_lib INT DEFAULT NULL
2519 )
2520 RETURNS XML AS $F$
2521      SELECT  XMLELEMENT(
2522                  name holdings,
2523                  XMLATTRIBUTES(
2524                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2525                     CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id,
2526                     (SELECT record_has_holdable_copy FROM asset.record_has_holdable_copy($1)) AS has_holdable
2527                  ),
2528                  XMLELEMENT(
2529                      name counts,
2530                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
2531                          SELECT  XMLELEMENT(
2532                                      name count,
2533                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2534                                  )::text
2535                            FROM  asset.opac_ou_record_copy_count($2,  $1)
2536                                      UNION
2537                          SELECT  XMLELEMENT(
2538                                      name count,
2539                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2540                                  )::text
2541                            FROM  asset.staff_ou_record_copy_count($2, $1)
2542                                      UNION
2543                          SELECT  XMLELEMENT(
2544                                      name count,
2545                                      XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2546                                  )::text
2547                            FROM  asset.opac_ou_record_copy_count($9,  $1)
2548                                      ORDER BY 1
2549                      )x)
2550                  ),
2551                  CASE
2552                      WHEN ('bmp' = ANY ($5)) THEN
2553                         XMLELEMENT(
2554                             name monograph_parts,
2555                             (SELECT XMLAGG(bmp) FROM (
2556                                 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)
2557                                   FROM  biblio.monograph_part
2558                                   WHERE record = $1
2559                             )x)
2560                         )
2561                      ELSE NULL
2562                  END,
2563                  XMLELEMENT(
2564                      name volumes,
2565                      (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
2566                         -- Physical copies
2567                         SELECT  unapi.acn(y.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
2568                         FROM evergreen.ranked_volumes($1, $2, $4, $6, $7, $9, $5) AS y
2569                         UNION ALL
2570                         -- Located URIs
2571                         SELECT unapi.acn(uris.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
2572                         FROM evergreen.located_uris($1, $2, $9) AS uris
2573                      )x)
2574                  ),
2575                  CASE WHEN ('ssub' = ANY ($5)) THEN
2576                      XMLELEMENT(
2577                          name subscriptions,
2578                          (SELECT XMLAGG(ssub) FROM (
2579                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
2580                               FROM  serial.subscription
2581                               WHERE record_entry = $1
2582                         )x)
2583                      )
2584                  ELSE NULL END,
2585                  CASE WHEN ('acp' = ANY ($5)) THEN
2586                      XMLELEMENT(
2587                          name foreign_copies,
2588                          (SELECT XMLAGG(acp) FROM (
2589                             SELECT  unapi.acp(p.target_copy,'xml','copy',evergreen.array_remove_item_by_value($5,'acp'), $3, $4, $6, $7, FALSE)
2590                               FROM  biblio.peer_bib_copy_map p
2591                                     JOIN asset.copy c ON (p.target_copy = c.id)
2592                               WHERE NOT c.deleted AND p.peer_record = $1
2593                             LIMIT ($6 -> 'acp')::INT
2594                             OFFSET ($7 -> 'acp')::INT
2595                         )x)
2596                      )
2597                  ELSE NULL END
2598              );
2599 $F$ LANGUAGE SQL STABLE;
2600
2601
2602
2603 SELECT evergreen.upgrade_deps_block_check('0858', :eg_version);
2604
2605 -- Fix faulty seed data. Otherwise for ptype 'f' we have subfield 'e'
2606 -- overlapping subfield 'd'
2607 UPDATE config.marc21_physical_characteristic_subfield_map
2608     SET start_pos = 5
2609     WHERE ptype_key = 'f' AND subfield = 'e';
2610
2611 -- Evergreen DB patch 0859.data.staff-initials-settings.sql
2612 --
2613 -- More granular configuration settings for requiring use of staff initials
2614 --
2615
2616 -- check whether patch can be applied
2617 SELECT evergreen.upgrade_deps_block_check('0859', :eg_version);
2618
2619 -- add new granular settings for requiring use of staff initials
2620 INSERT INTO config.org_unit_setting_type
2621     (name, grp, label, description, datatype)
2622     VALUES (
2623         'ui.staff.require_initials.patron_standing_penalty',
2624         'gui',
2625         oils_i18n_gettext(
2626             'ui.staff.require_initials.patron_standing_penalty',
2627             'Require staff initials for entry/edit of patron standing penalties and messages.',
2628             'coust',
2629             'label'
2630         ),
2631         oils_i18n_gettext(
2632             'ui.staff.require_initials.patron_standing_penalty',
2633             'Appends staff initials and edit date into patron standing penalties and messages.',
2634             'coust',
2635             'description'
2636         ),
2637         'bool'
2638     ), (
2639         'ui.staff.require_initials.patron_info_notes',
2640         'gui',
2641         oils_i18n_gettext(
2642             'ui.staff.require_initials.patron_info_notes',
2643             'Require staff initials for entry/edit of patron notes.',
2644             'coust',
2645             'label'
2646         ),
2647         oils_i18n_gettext(
2648             'ui.staff.require_initials.patron_info_notes',
2649             'Appends staff initials and edit date into patron note content.',
2650             'coust',
2651             'description'
2652         ),
2653         'bool'
2654     ), (
2655         'ui.staff.require_initials.copy_notes',
2656         'gui',
2657         oils_i18n_gettext(
2658             'ui.staff.require_initials.copy_notes',
2659             'Require staff initials for entry/edit of copy notes.',
2660             'coust',
2661             'label'
2662         ),
2663         oils_i18n_gettext(
2664             'ui.staff.require_initials.copy_notes',
2665             'Appends staff initials and edit date into copy note content..',
2666             'coust',
2667             'description'
2668         ),
2669         'bool'
2670     );
2671
2672 -- Update any existing setting so that the original set value is now passed to
2673 -- one of the newer settings.
2674
2675 UPDATE actor.org_unit_setting
2676 SET name = 'ui.staff.require_initials.patron_standing_penalty'
2677 WHERE name = 'ui.staff.require_initials';
2678
2679 -- Add similar values for new settings as old ones to preserve existing configured
2680 -- functionality.
2681
2682 INSERT INTO actor.org_unit_setting (org_unit, name, value)
2683 SELECT org_unit, 'ui.staff.require_initials.patron_info_notes', value
2684 FROM actor.org_unit_setting
2685 WHERE name = 'ui.staff.require_initials.patron_standing_penalty';
2686
2687 INSERT INTO actor.org_unit_setting (org_unit, name, value)
2688 SELECT org_unit, 'ui.staff.require_initials.copy_notes', value
2689 FROM actor.org_unit_setting
2690 WHERE name = 'ui.staff.require_initials.patron_standing_penalty';
2691
2692 -- Update setting logs so that the original setting name's history is now transferred
2693 -- over to one of the newer settings.
2694
2695 UPDATE config.org_unit_setting_type_log
2696 SET field_name = 'ui.staff.require_initials.patron_standing_penalty'
2697 WHERE field_name = 'ui.staff.require_initials';
2698
2699 -- Remove the old setting entirely
2700
2701 DELETE FROM config.org_unit_setting_type WHERE name = 'ui.staff.require_initials';
2702
2703
2704 -- oh, the irony
2705 SELECT evergreen.upgrade_deps_block_check('0860', :eg_version);
2706
2707 CREATE OR REPLACE FUNCTION evergreen.array_overlap_check (/* field */) RETURNS TRIGGER AS $$
2708 DECLARE
2709     fld     TEXT;
2710     cnt     INT;
2711 BEGIN
2712     fld := TG_ARGV[0];
2713     EXECUTE 'SELECT COUNT(*) FROM '|| TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME ||' WHERE '|| fld ||' && ($1).'|| fld INTO cnt USING NEW;
2714     IF cnt > 0 THEN
2715         RAISE EXCEPTION 'Cannot insert duplicate array into field % of table %', fld, TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME;
2716     END IF;
2717     RETURN NEW;
2718 END;
2719 $$ LANGUAGE PLPGSQL;
2720
2721
2722 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecates ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
2723     SELECT  DISTINCT l.version
2724       FROM  config.upgrade_log l
2725             JOIN config.db_patch_dependencies d ON (l.version = ANY(d.deprecates))
2726       WHERE d.db_patch = $1
2727 $$ LANGUAGE SQL;
2728
2729 -- List applied db patches that are superseded by (and block the application of) my_db_patch
2730 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_supersedes ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
2731     SELECT  DISTINCT l.version
2732       FROM  config.upgrade_log l
2733             JOIN config.db_patch_dependencies d ON (l.version = ANY(d.supersedes))
2734       WHERE d.db_patch = $1
2735 $$ LANGUAGE SQL;
2736
2737
2738 CREATE OR REPLACE FUNCTION evergreen.upgrade_deps_block_check ( my_db_patch TEXT, my_applied_to TEXT ) RETURNS BOOL AS $$
2739 DECLARE 
2740     deprecates TEXT;
2741     supersedes TEXT;
2742 BEGIN
2743     IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
2744         SELECT  STRING_AGG(patch, ', ') INTO deprecates FROM evergreen.upgrade_list_applied_deprecates(my_db_patch);
2745         SELECT  STRING_AGG(patch, ', ') INTO supersedes FROM evergreen.upgrade_list_applied_supersedes(my_db_patch);
2746         RAISE EXCEPTION '
2747 Upgrade script % can not be applied:
2748   applied deprecated scripts %
2749   applied superseded scripts %
2750   deprecated by %
2751   superseded by %',
2752             my_db_patch,
2753             (SELECT ARRAY_AGG(patch) FROM evergreen.upgrade_list_applied_deprecates(my_db_patch)),
2754             (SELECT ARRAY_AGG(patch) FROM evergreen.upgrade_list_applied_supersedes(my_db_patch)),
2755             evergreen.upgrade_list_applied_deprecated(my_db_patch),
2756             evergreen.upgrade_list_applied_superseded(my_db_patch);
2757     END IF;
2758
2759     INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
2760     RETURN TRUE;
2761 END;
2762 $$ LANGUAGE PLPGSQL;
2763
2764
2765
2766 SELECT evergreen.upgrade_deps_block_check('0861', :eg_version);
2767
2768 CREATE INDEX authority_record_entry_create_date_idx ON authority.record_entry ( create_date );
2769 CREATE INDEX authority_record_entry_edit_date_idx ON authority.record_entry ( edit_date );
2770
2771
2772
2773 SELECT evergreen.upgrade_deps_block_check('0863', :eg_version);
2774
2775
2776 -- cheat sheet for enabling Stripe payments:
2777 --  'credit.payments.allow' must be true, and among other things it drives the
2778 --      opac to render a payment form at all
2779 --  NEW 'credit.processor.stripe.enabled' must be true  (kind of redundant but
2780 --      my fault for setting the precedent with c.p.{authorizenet|paypal|payflowpro}.enabled)
2781 --  'credit.default.processor' must be 'Stripe'
2782 --  NEW 'credit.processor.stripe.pubkey' must be set
2783 --  NEW 'credit.processor.stripe.secretkey' must be set
2784
2785 INSERT into config.org_unit_setting_type
2786 ( name, grp, label, description, datatype, fm_class ) VALUES
2787
2788     ( 'credit.processor.stripe.enabled', 'credit',
2789     oils_i18n_gettext('credit.processor.stripe.enabled',
2790         'Enable Stripe payments',
2791         'coust', 'label'),
2792     oils_i18n_gettext('credit.processor.stripe.enabled',
2793         'Enable Stripe payments',
2794         'coust', 'description'),
2795     'bool', null)
2796
2797 ,( 'credit.processor.stripe.pubkey', 'credit',
2798     oils_i18n_gettext('credit.processor.stripe.pubkey',
2799         'Stripe publishable key',
2800         'coust', 'label'),
2801     oils_i18n_gettext('credit.processor.stripe.pubkey',
2802         'Stripe publishable key',
2803         'coust', 'description'),
2804     'string', null)
2805
2806 ,( 'credit.processor.stripe.secretkey', 'credit',
2807     oils_i18n_gettext('credit.processor.stripe.secretkey',
2808         'Stripe secret key',
2809         'coust', 'label'),
2810     oils_i18n_gettext('credit.processor.stripe.secretkey',
2811         'Stripe secret key',
2812         'coust', 'description'),
2813     'string', null)
2814 ;
2815
2816 UPDATE config.org_unit_setting_type
2817 SET description = 'This might be "AuthorizeNet", "PayPal", "PayflowPro", or "Stripe".'
2818 WHERE name = 'credit.processor.default' AND description = 'This might be "AuthorizeNet", "PayPal", etc.'; -- don't clobber local edits or i18n
2819
2820
2821 SELECT evergreen.upgrade_deps_block_check('0864', :eg_version);
2822
2823 CREATE EXTENSION intarray;
2824
2825 -- while we have this opportunity, and before we start collecting 
2826 -- CCVM IDs (below) carve out a nice space for stock ccvm values
2827 UPDATE config.coded_value_map SET id = id + 10000 WHERE id > 556;
2828 SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, 
2829     (SELECT GREATEST(max(id), 10000) FROM config.coded_value_map));
2830
2831 ALTER TABLE config.record_attr_definition ADD COLUMN multi BOOL NOT NULL DEFAULT TRUE, ADD COLUMN composite BOOL NOT NULL DEFAULT FALSE;
2832
2833 UPDATE  config.record_attr_definition
2834   SET   multi = FALSE
2835   WHERE name IN ('bib_level','control_type','pubdate','cat_form','enc_level','item_type','titlesort','authorsort');
2836
2837 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
2838 DECLARE
2839     rowid   INT := 0;
2840     _007    TEXT;
2841     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
2842     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
2843     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
2844     retval  biblio.marc21_physical_characteristics%ROWTYPE;
2845 BEGIN
2846
2847     FOR _007 IN SELECT oils_xpath_string('//*', value) FROM UNNEST(oils_xpath('//*[@tag="007"]', marc)) x(value) LOOP
2848         IF _007 IS NOT NULL AND _007 <> '' THEN
2849             SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
2850
2851             IF ptype.ptype_key IS NOT NULL THEN
2852                 FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
2853                     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 );
2854
2855                     IF pval.id IS NOT NULL THEN
2856                         rowid := rowid + 1;
2857                         retval.id := rowid;
2858                         retval.ptype := ptype.ptype_key;
2859                         retval.subfield := psf.id;
2860                         retval.value := pval.id;
2861                         RETURN NEXT retval;
2862                     END IF;
2863
2864                 END LOOP;
2865             END IF;
2866         END IF;
2867     END LOOP;
2868
2869     RETURN;
2870 END;
2871 $func$ LANGUAGE PLPGSQL;
2872
2873 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field_list( marc TEXT, ff TEXT ) RETURNS TEXT[] AS $func$
2874 DECLARE
2875     rtype       TEXT;
2876     ff_pos      RECORD;
2877     tag_data    RECORD;
2878     val         TEXT;
2879     collection  TEXT[] := '{}'::TEXT[];
2880 BEGIN
2881     rtype := (vandelay.marc21_record_type( marc )).code;
2882     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
2883         IF ff_pos.tag = 'ldr' THEN
2884             val := oils_xpath_string('//*[local-name()="leader"]', marc);
2885             IF val IS NOT NULL THEN
2886                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
2887                 collection := collection || val;
2888             END IF;
2889         ELSE
2890             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
2891                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
2892                 collection := collection || val;
2893             END LOOP;
2894         END IF;
2895         val := REPEAT( ff_pos.default_val, ff_pos.length );
2896         collection := collection || val;
2897     END LOOP;
2898
2899     RETURN collection;
2900 END;
2901 $func$ LANGUAGE PLPGSQL;
2902
2903 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field_list( rid BIGINT, ff TEXT ) RETURNS TEXT[] AS $func$
2904     SELECT * FROM vandelay.marc21_extract_fixed_field_list( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
2905 $func$ LANGUAGE SQL;
2906
2907 -- DECREMENTING serial starts at -1
2908 CREATE SEQUENCE metabib.uncontrolled_record_attr_value_id_seq INCREMENT BY -1;
2909
2910 CREATE TABLE metabib.uncontrolled_record_attr_value (
2911     id      BIGINT  PRIMARY KEY DEFAULT nextval('metabib.uncontrolled_record_attr_value_id_seq'),
2912     attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name),
2913     value   TEXT    NOT NULL
2914 );
2915 CREATE UNIQUE INDEX muv_once_idx ON metabib.uncontrolled_record_attr_value (attr,value);
2916
2917 CREATE TABLE metabib.record_attr_vector_list (
2918     source  BIGINT  PRIMARY KEY REFERENCES  biblio.record_entry (id),
2919     vlist   INT[]   NOT NULL -- stores id from ccvm AND murav
2920 );
2921 CREATE INDEX mrca_vlist_idx ON metabib.record_attr_vector_list USING gin ( vlist gin__int_ops );
2922
2923 CREATE TABLE metabib.record_sorter (
2924     id      BIGSERIAL   PRIMARY KEY,
2925     source  BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
2926     attr    TEXT        NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE,
2927     value   TEXT        NOT NULL
2928 );
2929 CREATE INDEX metabib_sorter_source_idx ON metabib.record_sorter (source); -- we may not need one of this or the next ... stats will tell
2930 CREATE INDEX metabib_sorter_s_a_idx ON metabib.record_sorter (source, attr);
2931 CREATE INDEX metabib_sorter_a_v_idx ON metabib.record_sorter (attr, value);
2932
2933 CREATE TEMP TABLE attr_set ON COMMIT DROP AS SELECT  DISTINCT id AS source, (each(attrs)).key,(each(attrs)).value FROM metabib.record_attr;
2934 DELETE FROM attr_set WHERE BTRIM(value) = '';
2935
2936 -- Grab sort values for the new sorting mechanism
2937 INSERT INTO metabib.record_sorter (source,attr,value)
2938     SELECT  a.source, a.key, a.value
2939       FROM  attr_set a
2940             JOIN config.record_attr_definition d ON (d.name = a.key AND d.sorter AND a.value IS NOT NULL);
2941
2942 -- Rewrite uncontrolled SVF record attrs as the seeds of an intarray vector
2943 INSERT INTO metabib.uncontrolled_record_attr_value (attr,value)
2944     SELECT  DISTINCT a.key, a.value
2945       FROM  attr_set a
2946             JOIN config.record_attr_definition d ON (d.name = a.key AND d.filter AND a.value IS NOT NULL)
2947             LEFT JOIN config.coded_value_map m ON (m.ctype = a.key)
2948       WHERE m.id IS NULL;
2949
2950 -- Now construct the record-specific vector from the SVF data
2951 INSERT INTO metabib.record_attr_vector_list (source,vlist)
2952     SELECT  a.id, ARRAY_AGG(COALESCE(u.id, c.id))
2953       FROM  metabib.record_attr a
2954             JOIN attr_set ON (a.id = attr_set.source)
2955             LEFT JOIN metabib.uncontrolled_record_attr_value u ON (u.attr = attr_set.key AND u.value = attr_set.value)
2956             LEFT JOIN config.coded_value_map c ON (c.ctype = attr_set.key AND c.code = attr_set.value)
2957       WHERE COALESCE(u.id,c.id) IS NOT NULL
2958       GROUP BY 1;
2959
2960 DROP VIEW IF EXISTS reporter.classic_current_circ; 
2961 DROP VIEW metabib.rec_descriptor;
2962 DROP TABLE metabib.record_attr;
2963
2964 CREATE TYPE metabib.record_attr_type AS (
2965     id      BIGINT,
2966     attrs   HSTORE
2967 );
2968
2969 CREATE TABLE config.composite_attr_entry_definition(
2970     coded_value INT  PRIMARY KEY NOT NULL REFERENCES config.coded_value_map (id) ON UPDATE CASCADE ON DELETE CASCADE,
2971     definition  TEXT NOT NULL -- JSON
2972 );
2973
2974 CREATE OR REPLACE VIEW metabib.record_attr_id_map AS
2975     SELECT id, attr, value FROM metabib.uncontrolled_record_attr_value
2976         UNION
2977     SELECT  c.id, c.ctype AS attr, c.code AS value
2978       FROM  config.coded_value_map c
2979             JOIN config.record_attr_definition d ON (d.name = c.ctype AND NOT d.composite);
2980
2981 CREATE VIEW metabib.composite_attr_id_map AS
2982     SELECT  c.id, c.ctype AS attr, c.code AS value
2983       FROM  config.coded_value_map c
2984             JOIN config.record_attr_definition d ON (d.name = c.ctype AND d.composite);
2985
2986 CREATE OR REPLACE VIEW metabib.full_attr_id_map AS
2987     SELECT id, attr, value FROM metabib.record_attr_id_map
2988         UNION
2989     SELECT id, attr, value FROM metabib.composite_attr_id_map;
2990
2991
2992 -- Back-compat view ... we're moving to an INTARRAY world
2993 CREATE VIEW metabib.record_attr_flat AS
2994     SELECT  v.source AS id,
2995             m.attr,
2996             m.value
2997       FROM  metabib.full_attr_id_map m
2998             JOIN  metabib.record_attr_vector_list v ON ( m.id = ANY( v.vlist ) );
2999
3000 CREATE VIEW metabib.record_attr AS
3001     SELECT id, HSTORE( ARRAY_AGG( attr ), ARRAY_AGG( value ) ) AS attrs FROM metabib.record_attr_flat GROUP BY 1;
3002
3003 CREATE VIEW metabib.rec_descriptor AS
3004     SELECT  id,
3005             id AS record,
3006             (populate_record(NULL::metabib.rec_desc_type, attrs)).*
3007       FROM  metabib.record_attr;
3008
3009 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_init () RETURNS BOOL AS $f$
3010     $_SHARED{metabib_compile_composite_attr_cache} = {}
3011         if ! exists $_SHARED{metabib_compile_composite_attr_cache};
3012     return exists $_SHARED{metabib_compile_composite_attr_cache};
3013 $f$ LANGUAGE PLPERLU;
3014
3015 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_disable () RETURNS BOOL AS $f$
3016     delete $_SHARED{metabib_compile_composite_attr_cache};
3017     return ! exists $_SHARED{metabib_compile_composite_attr_cache};
3018 $f$ LANGUAGE PLPERLU;
3019
3020 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_invalidate () RETURNS BOOL AS $f$
3021     SELECT metabib.compile_composite_attr_cache_disable() AND metabib.compile_composite_attr_cache_init();
3022 $f$ LANGUAGE SQL;
3023
3024 CREATE OR REPLACE FUNCTION metabib.composite_attr_def_cache_inval_tgr () RETURNS TRIGGER AS $f$
3025 BEGIN
3026     PERFORM metabib.compile_composite_attr_cache_invalidate();
3027     RETURN NULL;
3028 END;
3029 $f$ LANGUAGE PLPGSQL;
3030
3031 CREATE TRIGGER ccraed_cache_inval_tgr AFTER INSERT OR UPDATE OR DELETE ON config.composite_attr_entry_definition FOR EACH STATEMENT EXECUTE PROCEDURE metabib.composite_attr_def_cache_inval_tgr();
3032     
3033 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RETURNS query_int AS $func$
3034
3035     use JSON::XS;
3036
3037     my $json = shift;
3038     my $def = decode_json($json);
3039
3040     die("Composite attribute definition not supplied") unless $def;
3041
3042     my $_cache = (exists $_SHARED{metabib_compile_composite_attr_cache}) ? 1 : 0;
3043
3044     return $_SHARED{metabib_compile_composite_attr_cache}{$json}
3045         if ($_cache && $_SHARED{metabib_compile_composite_attr_cache}{$json});
3046
3047     sub recurse {
3048         my $d = shift;
3049         my $j = '&';
3050         my @list;
3051
3052         if (ref $d eq 'HASH') { # node or AND
3053             if (exists $d->{_attr}) { # it is a node
3054                 my $plan = spi_prepare('SELECT * FROM metabib.full_attr_id_map WHERE attr = $1 AND value = $2', qw/TEXT TEXT/);
3055                 my $id = spi_exec_prepared(
3056                     $plan, {limit => 1}, $d->{_attr}, $d->{_val}
3057                 )->{rows}[0]{id};
3058                 spi_freeplan($plan);
3059                 return $id;
3060             } elsif (exists $d->{_not} && scalar(keys(%$d)) == 1) { # it is a NOT
3061                 return '!' . recurse($$d{_not});
3062             } else { # an AND list
3063                 @list = map { recurse($$d{$_}) } sort keys %$d;
3064             }
3065         } elsif (ref $d eq 'ARRAY') {
3066             $j = '|';
3067             @list = map { recurse($_) } @$d;
3068         }
3069
3070         @list = grep { defined && $_ ne '' } @list;
3071
3072         return '(' . join($j,@list) . ')' if @list;
3073         return '';
3074     }
3075
3076     my $val = recurse($def) || undef;
3077     $_SHARED{metabib_compile_composite_attr_cache}{$json} = $val if $_cache;
3078     return $val;
3079
3080 $func$ IMMUTABLE LANGUAGE plperlu;
3081
3082 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_id INT ) RETURNS query_int AS $func$
3083     SELECT metabib.compile_composite_attr(definition) FROM config.composite_attr_entry_definition WHERE coded_value = $1;
3084 $func$ STRICT IMMUTABLE LANGUAGE SQL;
3085
3086
3087 CREATE OR REPLACE FUNCTION public.oils_tsearch2 () RETURNS TRIGGER AS $$
3088 DECLARE
3089     normalizer      RECORD;
3090     value           TEXT := '';
3091     temp_vector     TEXT := '';
3092     ts_rec          RECORD;
3093     cur_weight      "char";
3094 BEGIN
3095
3096     value := NEW.value;
3097     NEW.index_vector = ''::tsvector;
3098
3099     IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
3100         FOR normalizer IN
3101             SELECT  n.func AS func,
3102                     n.param_count AS param_count,
3103                     m.params AS params
3104               FROM  config.index_normalizer n
3105                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
3106               WHERE field = NEW.field AND m.pos < 0
3107               ORDER BY m.pos LOOP
3108                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
3109                     quote_literal( value ) ||
3110                     CASE
3111                         WHEN normalizer.param_count > 0
3112                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
3113                             ELSE ''
3114                         END ||
3115                     ')' INTO value;
3116
3117         END LOOP;
3118
3119         NEW.value = value;
3120
3121         FOR normalizer IN
3122             SELECT  n.func AS func,
3123                     n.param_count AS param_count,
3124                     m.params AS params
3125               FROM  config.index_normalizer n
3126                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
3127               WHERE field = NEW.field AND m.pos >= 0
3128               ORDER BY m.pos LOOP
3129                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
3130                     quote_literal( value ) ||
3131                     CASE
3132                         WHEN normalizer.param_count > 0
3133                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
3134                             ELSE ''
3135                         END ||
3136                     ')' INTO value;
3137
3138         END LOOP;
3139    END IF;
3140
3141     IF TG_TABLE_NAME::TEXT ~ 'browse_entry$' THEN
3142
3143         value :=  ARRAY_TO_STRING(
3144             evergreen.regexp_split_to_array(value, E'\\W+'), ' '
3145         );
3146         value := public.search_normalize(value);
3147         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
3148
3149     ELSIF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
3150         FOR ts_rec IN
3151
3152             SELECT DISTINCT m.ts_config, m.index_weight
3153             FROM config.metabib_class_ts_map m
3154                  LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
3155                  LEFT JOIN config.coded_value_map ccvm ON (
3156                     ccvm.ctype IN ('item_lang', 'language') AND
3157                     ccvm.code = m.index_lang AND
3158                     r.vlist @> intset(ccvm.id)
3159                 )
3160             WHERE m.field_class = TG_ARGV[0]
3161                 AND m.active
3162                 AND (m.always OR NOT EXISTS (SELECT 1 FROM config.metabib_field_ts_map WHERE metabib_field = NEW.field))
3163                 AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
3164                         UNION
3165             SELECT DISTINCT m.ts_config, m.index_weight
3166             FROM config.metabib_field_ts_map m
3167                  LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
3168                  LEFT JOIN config.coded_value_map ccvm ON (
3169                     ccvm.ctype IN ('item_lang', 'language') AND
3170                     ccvm.code = m.index_lang AND
3171                     r.vlist @> intset(ccvm.id)
3172                 )
3173             WHERE m.metabib_field = NEW.field
3174                 AND m.active
3175                 AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
3176             ORDER BY index_weight ASC
3177
3178         LOOP
3179
3180             IF cur_weight IS NOT NULL AND cur_weight != ts_rec.index_weight THEN
3181                 NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
3182                 temp_vector = '';
3183             END IF;
3184
3185             cur_weight = ts_rec.index_weight;
3186             SELECT INTO temp_vector temp_vector || ' ' || to_tsvector(ts_rec.ts_config::regconfig, value)::TEXT;
3187
3188         END LOOP;
3189         NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
3190     ELSE
3191         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
3192     END IF;
3193
3194     RETURN NEW;
3195 END;
3196 $$ LANGUAGE PLPGSQL;
3197
3198 -- add new sr_format attribute definition
3199
3200 INSERT INTO config.record_attr_definition (name, label, phys_char_sf)
3201 VALUES (
3202     'sr_format', 
3203     oils_i18n_gettext('sr_format', 'Sound recording format', 'crad', 'label'),
3204     '62'
3205 );
3206
3207 INSERT INTO config.coded_value_map (id, ctype, code, value) VALUES
3208 (557, 'sr_format', 'a', oils_i18n_gettext(557, '16 rpm', 'ccvm', 'value')),
3209 (558, 'sr_format', 'b', oils_i18n_gettext(558, '33 1/3 rpm', 'ccvm', 'value')),
3210 (559, 'sr_format', 'c', oils_i18n_gettext(559, '45 rpm', 'ccvm', 'value')),
3211 (560, 'sr_format', 'f', oils_i18n_gettext(560, '1.4 m. per second', 'ccvm', 'value')),
3212 (561, 'sr_format', 'd', oils_i18n_gettext(561, '78 rpm', 'ccvm', 'value')),
3213 (562, 'sr_format', 'e', oils_i18n_gettext(562, '8 rpm', 'ccvm', 'value')),
3214 (563, 'sr_format', 'l', oils_i18n_gettext(563, '1 7/8 ips', 'ccvm', 'value')),
3215 (586, 'item_form', 'o', oils_i18n_gettext('586', 'Online', 'ccvm', 'value')),
3216 (587, 'item_form', 'q', oils_i18n_gettext('587', 'Direct electronic', 'ccvm', 'value'));
3217
3218 INSERT INTO config.coded_value_map
3219     (id, ctype, code, value, search_label) VALUES 
3220 (564, 'icon_format', 'book', 
3221     oils_i18n_gettext(564, 'Book', 'ccvm', 'value'),
3222     oils_i18n_gettext(564, 'Book', 'ccvm', 'search_label')),
3223 (565, 'icon_format', 'braille', 
3224     oils_i18n_gettext(565, 'Braille', 'ccvm', 'value'),
3225     oils_i18n_gettext(565, 'Braille', 'ccvm', 'search_label')),
3226 (566, 'icon_format', 'software', 
3227     oils_i18n_gettext(566, 'Software and video games', 'ccvm', 'value'),
3228     oils_i18n_gettext(566, 'Software and video games', 'ccvm', 'search_label')),
3229 (567, 'icon_format', 'dvd', 
3230     oils_i18n_gettext(567, 'DVD', 'ccvm', 'value'),
3231     oils_i18n_gettext(567, 'DVD', 'ccvm', 'search_label')),
3232 (568, 'icon_format', 'ebook', 
3233     oils_i18n_gettext(568, 'E-book', 'ccvm', 'value'),
3234     oils_i18n_gettext(568, 'E-book', 'ccvm', 'search_label')),
3235 (569, 'icon_format', 'eaudio', 
3236     oils_i18n_gettext(569, 'E-audio', 'ccvm', 'value'),
3237     oils_i18n_gettext(569, 'E-audio', 'ccvm', 'search_label')),
3238 (570, 'icon_format', 'kit', 
3239     oils_i18n_gettext(570, 'Kit', 'ccvm', 'value'),
3240     oils_i18n_gettext(570, 'Kit', 'ccvm', 'search_label')),
3241 (571, 'icon_format', 'map', 
3242     oils_i18n_gettext(571, 'Map', 'ccvm', 'value'),
3243     oils_i18n_gettext(571, 'Map', 'ccvm', 'search_label')),
3244 (572, 'icon_format', 'microform', 
3245     oils_i18n_gettext(572, 'Microform', 'ccvm', 'value'),
3246     oils_i18n_gettext(572, 'Microform', 'ccvm', 'search_label')),
3247 (573, 'icon_format', 'score', 
3248     oils_i18n_gettext(573, 'Music Score', 'ccvm', 'value'),
3249     oils_i18n_gettext(573, 'Music Score', 'ccvm', 'search_label')),
3250 (574, 'icon_format', 'picture', 
3251     oils_i18n_gettext(574, 'Picture', 'ccvm', 'value'),
3252     oils_i18n_gettext(574, 'Picture', 'ccvm', 'search_label')),
3253 (575, 'icon_format', 'equip', 
3254     oils_i18n_gettext(575, 'Equipment, games, toys', 'ccvm', 'value'),
3255     oils_i18n_gettext(575, 'Equipment, games, toys', 'ccvm', 'search_label')),
3256 (576, 'icon_format', 'serial', 
3257     oils_i18n_gettext(576, 'Serials and magazines', 'ccvm', 'value'),
3258     oils_i18n_gettext(576, 'Serials and magazines', 'ccvm', 'search_label')),
3259 (577, 'icon_format', 'vhs', 
3260     oils_i18n_gettext(577, 'VHS', 'ccvm', 'value'),
3261     oils_i18n_gettext(577, 'VHS', 'ccvm', 'search_label')),
3262 (578, 'icon_format', 'evideo', 
3263     oils_i18n_gettext(578, 'E-video', 'ccvm', 'value'),
3264     oils_i18n_gettext(578, 'E-video', 'ccvm', 'search_label')),
3265 (579, 'icon_format', 'cdaudiobook', 
3266     oils_i18n_gettext(579, 'CD Audiobook', 'ccvm', 'value'),
3267     oils_i18n_gettext(579, 'CD Audiobook', 'ccvm', 'search_label')),
3268 (580, 'icon_format', 'cdmusic', 
3269     oils_i18n_gettext(580, 'CD Music recording', 'ccvm', 'value'),
3270     oils_i18n_gettext(580, 'CD Music recording', 'ccvm', 'search_label')),
3271 (581, 'icon_format', 'casaudiobook', 
3272     oils_i18n_gettext(581, 'Cassette audiobook', 'ccvm', 'value'),
3273     oils_i18n_gettext(581, 'Cassette audiobook', 'ccvm', 'search_label')),
3274 (582, 'icon_format', 'casmusic',
3275     oils_i18n_gettext(582, 'Audiocassette music recording', 'ccvm', 'value'),
3276     oils_i18n_gettext(582, 'Audiocassette music recording', 'ccvm', 'search_label')),
3277 (583, 'icon_format', 'phonospoken', 
3278     oils_i18n_gettext(583, 'Phonograph spoken recording', 'ccvm', 'value'),
3279     oils_i18n_gettext(583, 'Phonograph spoken recording', 'ccvm', 'search_label')),
3280 (584, 'icon_format', 'phonomusic', 
3281     oils_i18n_gettext(584, 'Phonograph music recording', 'ccvm', 'value'),
3282     oils_i18n_gettext(584, 'Phonograph music recording', 'ccvm', 'search_label')),
3283 (585, 'icon_format', 'lpbook', 
3284     oils_i18n_gettext(585, 'Large Print Book', 'ccvm', 'value'),
3285     oils_i18n_gettext(585, 'Large Print Book', 'ccvm', 'search_label'))
3286 ;
3287
3288 -- add the new icon format attribute definition
3289
3290 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
3291     'opac.icon_attr',
3292     oils_i18n_gettext(
3293         'opac.icon_attr', 
3294         'OPAC Format Icons Attribute',
3295         'cgf',
3296         'label'
3297     ),
3298     'icon_format', 
3299     TRUE
3300 );
3301
3302 INSERT INTO config.record_attr_definition 
3303     (name, label, multi, filter, composite) VALUES (
3304     'icon_format',
3305     oils_i18n_gettext(
3306         'icon_format',
3307         'OPAC Format Icons',
3308         'crad',
3309         'label'
3310     ),
3311     TRUE, TRUE, TRUE
3312 );
3313
3314 -- icon format composite definitions
3315
3316 INSERT INTO config.composite_attr_entry_definition 
3317     (coded_value, definition) VALUES
3318 --book
3319 (564, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"d"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
3320
3321 -- braille
3322 (565, '{"0":{"_attr":"item_type","_val":"a"},"1":{"_attr":"item_form","_val":"f"}}'),
3323
3324 -- software
3325 (566, '{"_attr":"item_type","_val":"m"}'),
3326
3327 -- dvd
3328 (567, '{"_attr":"vr_format","_val":"v"}'),
3329
3330 -- ebook
3331 (568, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}],"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'),
3332
3333 -- eaudio
3334 (569, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"s"}]}'),
3335
3336 -- kit
3337 (570, '[{"_attr":"item_type","_val":"o"},{"_attr":"item_type","_val":"p"}]'),
3338
3339 -- map
3340 (571, '[{"_attr":"item_type","_val":"e"},{"_attr":"item_type","_val":"f"}]'),
3341
3342 -- microform
3343 (572, '[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"}]'),
3344
3345 -- score
3346 (573, '[{"_attr":"item_type","_val":"c"},{"_attr":"item_type","_val":"d"}]'),
3347
3348 -- picture
3349 (574, '{"_attr":"item_type","_val":"k"}'),
3350
3351 -- equip
3352 (575, '{"_attr":"item_type","_val":"r"}'),
3353
3354 -- serial
3355 (576, '[{"_attr":"bib_level","_val":"b"},{"_attr":"bib_level","_val":"s"}]'),
3356
3357 -- vhs
3358 (577, '{"_attr":"vr_format","_val":"b"}'),
3359
3360 -- evideo
3361 (578, '{"0":{"_attr":"item_type","_val":"g"},"1":[{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"s"},{"_attr":"item_form","_val":"q"}]}'),
3362
3363 -- cdaudiobook
3364 (579, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"f"}}'),
3365
3366 -- cdmusic
3367 (580, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"f"}}'),
3368
3369 -- casaudiobook
3370 (581, '{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"sr_format","_val":"l"}}'),
3371
3372 -- casmusic
3373 (582, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_attr":"sr_format","_val":"l"}}'),
3374
3375 -- phonospoken
3376 (583, '{"0":{"_attr":"item_type","_val":"i"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
3377
3378 -- phonomusic
3379 (584, '{"0":{"_attr":"item_type","_val":"j"},"1":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"e"}]}'),
3380
3381 -- lpbook
3382 (585, '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_attr":"item_form","_val":"d"},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}');
3383
3384
3385
3386
3387 CREATE OR REPLACE FUNCTION unapi.mra (
3388     obj_id BIGINT,
3389     format TEXT,
3390     ename TEXT,
3391     includes TEXT[],
3392     org TEXT,
3393     depth INT DEFAULT NULL,
3394     slimit HSTORE DEFAULT NULL,
3395     soffset HSTORE DEFAULT NULL,
3396     include_xmlns BOOL DEFAULT TRUE
3397 ) RETURNS XML AS $F$
3398     SELECT  XMLELEMENT(
3399         name attributes,
3400         XMLATTRIBUTES(
3401             CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
3402             'tag:open-ils.org:U2@mra/' || $1 AS id, 
3403             'tag:open-ils.org:U2@bre/' || $1 AS record 
3404         ),  
3405         (SELECT XMLAGG(foo.y)
3406           FROM (
3407             SELECT  XMLELEMENT(
3408                         name field,
3409                         XMLATTRIBUTES(
3410                             mra.attr AS name,
3411                             cvm.value AS "coded-value",
3412                             cvm.id AS "cvmid",
3413                             rad.composite,
3414                             rad.multi,
3415                             rad.filter,
3416                             rad.sorter
3417                         ),
3418                         mra.value
3419                     )
3420               FROM  metabib.record_attr_flat mra
3421                     JOIN config.record_attr_definition rad ON (mra.attr = rad.name)
3422                     LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = mra.attr AND code = mra.value)
3423               WHERE mra.id = $1
3424             )foo(y)
3425         )   
3426     )   
3427 $F$ LANGUAGE SQL STABLE;
3428
3429
3430 SELECT evergreen.upgrade_deps_block_check('0865', :eg_version);
3431
3432 -- First, explode the field into constituent parts
3433 WITH format_parts_array AS (
3434     SELECT  a.id,
3435             STRING_TO_ARRAY(a.holdable_formats, '-') AS parts
3436       FROM  action.hold_request a
3437       WHERE a.hold_type = 'M'
3438             AND a.fulfillment_time IS NULL
3439 ), format_parts_wide AS (
3440     SELECT  id,
3441             regexp_split_to_array(parts[1], '') AS item_type,
3442             regexp_split_to_array(parts[2], '') AS item_form,
3443             parts[3] AS item_lang
3444       FROM  format_parts_array
3445 ), converted_formats_flat AS (
3446     SELECT  id, 
3447             CASE WHEN ARRAY_LENGTH(item_type,1) > 0
3448                 THEN '"0":[{"_attr":"item_type","_val":"' || ARRAY_TO_STRING(item_type,'"},{"_attr":"item_type","_val":"') || '"}]'
3449                 ELSE '"0":""'
3450             END AS item_type,
3451             CASE WHEN ARRAY_LENGTH(item_form,1) > 0
3452                 THEN '"1":[{"_attr":"item_form","_val":"' || ARRAY_TO_STRING(item_form,'"},{"_attr":"item_form","_val":"') || '"}]'
3453                 ELSE '"1":""'
3454             END AS item_form,
3455             CASE WHEN item_lang <> ''
3456                 THEN '"2":[{"_attr":"item_lang","_val":"' || item_lang ||'"}]'
3457                 ELSE '"2":""'
3458             END AS item_lang
3459       FROM  format_parts_wide
3460 ) UPDATE action.hold_request SET holdable_formats = '{' ||
3461         converted_formats_flat.item_type || ',' ||
3462         converted_formats_flat.item_form || ',' ||
3463         converted_formats_flat.item_lang || '}'
3464     FROM converted_formats_flat WHERE converted_formats_flat.id = action.hold_request.id;
3465
3466
3467
3468 SELECT evergreen.upgrade_deps_block_check('0866', :eg_version);
3469
3470 DROP FUNCTION asset.record_has_holdable_copy (BIGINT);
3471 CREATE FUNCTION asset.record_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$
3472 BEGIN
3473     PERFORM 1
3474         FROM
3475             asset.copy acp
3476             JOIN asset.call_number acn ON acp.call_number = acn.id
3477             JOIN asset.copy_location acpl ON acp.location = acpl.id
3478             JOIN config.copy_status ccs ON acp.status = ccs.id
3479         WHERE
3480             acn.record = rid
3481             AND acp.holdable = true
3482             AND acpl.holdable = true
3483             AND ccs.holdable = true
3484             AND acp.deleted = false
3485             AND acp.circ_lib IN (SELECT id FROM actor.org_unit_descendants(COALESCE($2,(SELECT id FROM evergreen.org_top()))))
3486         LIMIT 1;
3487     IF FOUND THEN
3488         RETURN true;
3489     END IF;
3490     RETURN FALSE;
3491 END;
3492 $f$ LANGUAGE PLPGSQL;
3493
3494 DROP FUNCTION asset.metarecord_has_holdable_copy (BIGINT);
3495 CREATE FUNCTION asset.metarecord_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$
3496 BEGIN
3497     PERFORM 1
3498         FROM
3499             asset.copy acp
3500             JOIN asset.call_number acn ON acp.call_number = acn.id
3501             JOIN asset.copy_location acpl ON acp.location = acpl.id
3502             JOIN config.copy_status ccs ON acp.status = ccs.id
3503             JOIN metabib.metarecord_source_map mmsm ON acn.record = mmsm.source
3504         WHERE
3505             mmsm.metarecord = rid
3506             AND acp.holdable = true
3507             AND acpl.holdable = true
3508             AND ccs.holdable = true
3509             AND acp.deleted = false
3510             AND acp.circ_lib IN (SELECT id FROM actor.org_unit_descendants(COALESCE($2,(SELECT id FROM evergreen.org_top()))))
3511         LIMIT 1;
3512     IF FOUND THEN
3513         RETURN true;
3514     END IF;
3515     RETURN FALSE;
3516 END;
3517 $f$ LANGUAGE PLPGSQL;
3518
3519 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$
3520 DECLARE
3521     ans RECORD;
3522     trans INT;
3523 BEGIN
3524     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
3525
3526     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
3527         RETURN QUERY
3528         SELECT  ans.depth,
3529                 ans.id,
3530                 COUNT( av.id ),
3531                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
3532                 COUNT( av.id ),
3533                 trans
3534           FROM  
3535                 actor.org_unit_descendants(ans.id) d
3536                 JOIN asset.opac_visible_copies av ON (av.circ_lib = d.id)
3537                 JOIN asset.copy cp ON (cp.id = av.copy_id)
3538                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
3539           GROUP BY 1,2,6;
3540
3541         IF NOT FOUND THEN
3542             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
3543         END IF;
3544
3545     END LOOP;
3546
3547     RETURN;
3548 END;
3549 $f$ LANGUAGE PLPGSQL;
3550
3551 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$
3552 DECLARE
3553     ans RECORD;
3554     trans INT;
3555 BEGIN
3556     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
3557
3558     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
3559         RETURN QUERY
3560         SELECT  -1,
3561                 ans.id,
3562                 COUNT( av.id ),
3563                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
3564                 COUNT( av.id ),
3565                 trans
3566           FROM
3567                 actor.org_unit_descendants(ans.id) d
3568                 JOIN asset.opac_visible_copies av ON (av.circ_lib = d.id)
3569                 JOIN asset.copy cp ON (cp.id = av.copy_id)
3570                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
3571           GROUP BY 1,2,6;
3572
3573         IF NOT FOUND THEN
3574             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
3575         END IF;
3576
3577     END LOOP;   
3578                 
3579     RETURN;     
3580 END;            
3581 $f$ LANGUAGE PLPGSQL;
3582
3583 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$
3584 DECLARE         
3585     ans RECORD; 
3586     trans INT;
3587 BEGIN
3588     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
3589
3590     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
3591         RETURN QUERY
3592         SELECT  ans.depth,
3593                 ans.id,
3594                 COUNT( cp.id ),
3595                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
3596                 COUNT( cp.id ),
3597                 trans
3598           FROM
3599                 actor.org_unit_descendants(ans.id) d
3600                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
3601                 JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted)
3602                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = cn.record)
3603           GROUP BY 1,2,6;
3604
3605         IF NOT FOUND THEN
3606             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
3607         END IF;
3608
3609     END LOOP;
3610
3611     RETURN;
3612 END;
3613 $f$ LANGUAGE PLPGSQL;
3614
3615 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$
3616 DECLARE
3617     ans RECORD;
3618     trans INT;
3619 BEGIN
3620     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
3621
3622     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
3623         RETURN QUERY
3624         SELECT  -1,
3625                 ans.id,
3626                 COUNT( cp.id ),
3627                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
3628                 COUNT( cp.id ),
3629                 trans
3630           FROM
3631                 actor.org_unit_descendants(ans.id) d
3632                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
3633                 JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted)
3634                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = cn.record)
3635           GROUP BY 1,2,6;
3636
3637         IF NOT FOUND THEN
3638             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
3639         END IF;
3640
3641     END LOOP;
3642
3643     RETURN;
3644 END;
3645 $f$ LANGUAGE PLPGSQL;
3646
3647 CREATE OR REPLACE FUNCTION unapi.mmr_mra (
3648     obj_id BIGINT,
3649     format TEXT,
3650     ename TEXT,
3651     includes TEXT[],
3652     org TEXT,
3653     depth INT DEFAULT NULL,
3654     slimit HSTORE DEFAULT NULL,
3655     soffset HSTORE DEFAULT NULL,
3656     include_xmlns BOOL DEFAULT TRUE,
3657     pref_lib INT DEFAULT NULL
3658 ) RETURNS XML AS $F$
3659     SELECT  XMLELEMENT(
3660         name attributes,
3661         XMLATTRIBUTES(
3662             CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
3663             'tag:open-ils.org:U2@mmr/' || $1 AS metarecord
3664         ),
3665         (SELECT XMLAGG(foo.y)
3666           FROM (
3667             SELECT  DISTINCT ON (COALESCE(cvm.id,uvm.id))
3668                     COALESCE(cvm.id,uvm.id),
3669                     XMLELEMENT(
3670                         name field,
3671                         XMLATTRIBUTES(
3672                             mra.attr AS name,
3673                             cvm.value AS "coded-value",
3674                             cvm.id AS "cvmid",
3675                             rad.composite,
3676                             rad.multi,
3677                             rad.filter,
3678                             rad.sorter
3679                         ),
3680                         mra.value
3681                     )
3682               FROM  metabib.record_attr_flat mra
3683                     JOIN config.record_attr_definition rad ON (mra.attr = rad.name)
3684                     LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = mra.attr AND code = mra.value)
3685                     LEFT JOIN metabib.uncontrolled_record_attr_value uvm ON (uvm.attr = mra.attr AND uvm.value = mra.value)
3686               WHERE mra.id IN (
3687                     WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id 
3688                         FROM actor.org_unit WHERE shortname = $5 LIMIT 1)
3689                     SELECT source 
3690                     FROM metabib.metarecord_source_map, aou
3691                     WHERE metarecord = $1 AND (
3692                         EXISTS (
3693                             SELECT 1 FROM asset.opac_visible_copies 
3694                             WHERE record = source AND circ_lib IN (
3695                                 SELECT id FROM actor.org_unit_descendants(aou.id, $6)) 
3696                             LIMIT 1
3697                         )
3698                         OR EXISTS (SELECT 1 FROM located_uris(source, aou.id, $10) LIMIT 1)
3699                     )
3700                 )
3701               ORDER BY 1
3702             )foo(id,y)
3703         )
3704     )
3705 $F$ LANGUAGE SQL STABLE;
3706
3707 CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
3708     bibid BIGINT[],
3709     ouid INT,
3710     depth INT DEFAULT NULL,
3711     slimit HSTORE DEFAULT NULL,
3712     soffset HSTORE DEFAULT NULL,
3713     pref_lib INT DEFAULT NULL,
3714     includes TEXT[] DEFAULT NULL::TEXT[]
3715 ) RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT) AS $$
3716     SELECT ua.id, ua.name, ua.label_sortkey, MIN(ua.rank) AS rank FROM (
3717         SELECT acn.id, aou.name, acn.label_sortkey,
3718             evergreen.rank_ou(aou.id, $2, $6), evergreen.rank_cp_status(acp.status),
3719             RANK() OVER w
3720         FROM asset.call_number acn
3721             JOIN asset.copy acp ON (acn.id = acp.call_number)
3722             JOIN actor.org_unit_descendants( $2, COALESCE(
3723                 $3, (
3724                     SELECT depth
3725                     FROM actor.org_unit_type aout
3726                         INNER JOIN actor.org_unit ou ON ou_type = aout.id
3727                     WHERE ou.id = $2
3728                 ), $6)
3729             ) AS aou ON (acp.circ_lib = aou.id)
3730         WHERE acn.record = ANY ($1)
3731             AND acn.deleted IS FALSE
3732             AND acp.deleted IS FALSE
3733             AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN
3734                 EXISTS (
3735                     SELECT 1
3736                     FROM asset.opac_visible_copies
3737                     WHERE copy_id = acp.id AND record = acn.record
3738                 ) ELSE TRUE END
3739         GROUP BY acn.id, acp.status, aou.name, acn.label_sortkey, aou.id
3740         WINDOW w AS (
3741             ORDER BY evergreen.rank_ou(aou.id, $2, $6), evergreen.rank_cp_status(acp.status)
3742         )
3743     ) AS ua
3744     GROUP BY ua.id, ua.name, ua.label_sortkey
3745     ORDER BY rank, ua.name, ua.label_sortkey
3746     LIMIT ($4 -> 'acn')::INT
3747     OFFSET ($5 -> 'acn')::INT;
3748 $$
3749 LANGUAGE SQL STABLE;
3750
3751 CREATE OR REPLACE FUNCTION evergreen.ranked_volumes
3752     ( bibid BIGINT, ouid INT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, pref_lib INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[] )
3753     RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT)
3754     AS $$ SELECT * FROM evergreen.ranked_volumes(ARRAY[$1],$2,$3,$4,$5,$6,$7) $$ LANGUAGE SQL STABLE;
3755
3756
3757 CREATE OR REPLACE FUNCTION evergreen.located_uris (
3758     bibid BIGINT[],
3759     ouid INT,
3760     pref_lib INT DEFAULT NULL
3761 ) RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank INT) AS $$
3762     WITH all_orgs AS (SELECT COALESCE( enabled, FALSE ) AS flag FROM config.global_flag WHERE name = 'opac.located_uri.act_as_copy')
3763     SELECT DISTINCT ON (id) * FROM (
3764     SELECT acn.id, COALESCE(aou.name,aoud.name), acn.label_sortkey, evergreen.rank_ou(aou.id, $2, $3) AS pref_ou
3765       FROM asset.call_number acn
3766            INNER JOIN asset.uri_call_number_map auricnm ON acn.id = auricnm.call_number
3767            INNER JOIN asset.uri auri ON auri.id = auricnm.uri
3768            LEFT JOIN actor.org_unit_ancestors( COALESCE($3, $2) ) aou ON (acn.owning_lib = aou.id)
3769            LEFT JOIN actor.org_unit_descendants( COALESCE($3, $2) ) aoud ON (acn.owning_lib = aoud.id),
3770            all_orgs
3771       WHERE acn.record = ANY ($1)
3772           AND acn.deleted IS FALSE
3773           AND auri.active IS TRUE
3774           AND ((NOT all_orgs.flag AND aou.id IS NOT NULL) OR COALESCE(aou.id,aoud.id) IS NOT NULL)
3775     UNION
3776     SELECT acn.id, COALESCE(aou.name,aoud.name) AS name, acn.label_sortkey, evergreen.rank_ou(aou.id, $2, $3) AS pref_ou
3777       FROM asset.call_number acn
3778            INNER JOIN asset.uri_call_number_map auricnm ON acn.id = auricnm.call_number
3779            INNER JOIN asset.uri auri ON auri.id = auricnm.uri
3780            LEFT JOIN actor.org_unit_ancestors( $2 ) aou ON (acn.owning_lib = aou.id)
3781            LEFT JOIN actor.org_unit_descendants( $2 ) aoud ON (acn.owning_lib = aoud.id),
3782            all_orgs
3783       WHERE acn.record = ANY ($1)
3784           AND acn.deleted IS FALSE
3785           AND auri.active IS TRUE
3786           AND ((NOT all_orgs.flag AND aou.id IS NOT NULL) OR COALESCE(aou.id,aoud.id) IS NOT NULL))x
3787     ORDER BY id, pref_ou DESC;
3788 $$
3789 LANGUAGE SQL STABLE;
3790
3791 CREATE OR REPLACE FUNCTION evergreen.located_uris ( bibid BIGINT, ouid INT, pref_lib INT DEFAULT NULL)
3792     RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank INT)
3793     AS $$ SELECT * FROM evergreen.located_uris(ARRAY[$1],$2,$3) $$ LANGUAGE SQL STABLE;
3794
3795
3796 CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml (
3797     mid BIGINT,
3798     ouid INT,
3799     org TEXT,
3800     depth INT DEFAULT NULL,
3801     includes TEXT[] DEFAULT NULL::TEXT[],
3802     slimit HSTORE DEFAULT NULL,
3803     soffset HSTORE DEFAULT NULL,
3804     include_xmlns BOOL DEFAULT TRUE,
3805     pref_lib INT DEFAULT NULL
3806 )
3807 RETURNS XML AS $F$
3808      SELECT  XMLELEMENT(
3809                  name holdings,
3810                  XMLATTRIBUTES(
3811                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3812                     CASE WHEN ('mmr' = ANY ($5)) THEN 'tag:open-ils.org:U2@mmr/' || $1 || '/' || $3 ELSE NULL END AS id,
3813                     (SELECT metarecord_has_holdable_copy FROM asset.metarecord_has_holdable_copy($1)) AS has_holdable
3814                  ),
3815                  XMLELEMENT(
3816                      name counts,
3817                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
3818                          SELECT  XMLELEMENT(
3819                                      name count,
3820                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
3821                                  )::text
3822                            FROM  asset.opac_ou_metarecord_copy_count($2,  $1)
3823                                      UNION
3824                          SELECT  XMLELEMENT(
3825                                      name count,
3826                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
3827                                  )::text
3828                            FROM  asset.staff_ou_metarecord_copy_count($2, $1)
3829                                      UNION
3830                          SELECT  XMLELEMENT(
3831                                      name count,
3832                                      XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
3833                                  )::text
3834                            FROM  asset.opac_ou_metarecord_copy_count($9,  $1)
3835                                      ORDER BY 1
3836                      )x)
3837                  ),
3838                  -- XXX monograph_parts and foreign_copies are skipped in MRs ... put them back some day?
3839                  XMLELEMENT(
3840                      name volumes,
3841                      (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
3842                         -- Physical copies
3843                         SELECT  unapi.acn(y.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
3844                         FROM evergreen.ranked_volumes((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $4, $6, $7, $9, $5) AS y
3845                         UNION ALL
3846                         -- Located URIs
3847                         SELECT unapi.acn(uris.id,'xml','volume',evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
3848                         FROM evergreen.located_uris((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $9) AS uris
3849                      )x)
3850                  ),
3851                  CASE WHEN ('ssub' = ANY ($5)) THEN
3852                      XMLELEMENT(
3853                          name subscriptions,
3854                          (SELECT XMLAGG(ssub) FROM (
3855                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
3856                               FROM  serial.subscription
3857                               WHERE record_entry IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
3858                         )x)
3859                      )
3860                  ELSE NULL END
3861              );
3862 $F$ LANGUAGE SQL STABLE;
3863
3864
3865
3866 SELECT evergreen.upgrade_deps_block_check('0867', :eg_version);
3867
3868 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
3869     'opac.metarecord.holds.format_attr', 
3870     oils_i18n_gettext(
3871         'opac.metarecord.holds.format_attr',
3872         'OPAC Metarecord Hold Formats Attribute', 
3873         'cgf',
3874         'label'
3875     ),
3876     'mr_hold_format', 
3877     TRUE
3878 );
3879
3880 -- until we have a custom attribute for the selector, 
3881 -- default to the icon_format attribute
3882 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
3883     'opac.format_selector.attr', 
3884     oils_i18n_gettext(
3885         'opac.format_selector.attr', 
3886         'OPAC Format Selector Attribute', 
3887         'cgf',
3888         'label'
3889     ),
3890     'icon_format', 
3891     TRUE
3892 );
3893
3894
3895 INSERT INTO config.record_attr_definition 
3896     (name, label, multi, filter, composite) 
3897 VALUES (
3898     'mr_hold_format', 
3899     oils_i18n_gettext(
3900         'mr_hold_format',
3901         'Metarecord Hold Formats', 
3902         'crad',
3903         'label'
3904     ),
3905     TRUE, TRUE, TRUE
3906 );
3907
3908 -- these formats are a subset of the "icon_format" attribute,
3909 -- modified to exclude electronic resources, which are not holdable
3910
3911 -- for i18n purposes, these have to be listed individually
3912 INSERT INTO config.coded_value_map
3913     (id, ctype, code, value, search_label) VALUES 
3914 (588, 'mr_hold_format', 'book', 
3915     oils_i18n_gettext(588, 'Book', 'ccvm', 'value'),
3916     oils_i18n_gettext(588, 'Book', 'ccvm', 'search_label')),
3917 (589, 'mr_hold_format', 'braille', 
3918     oils_i18n_gettext(589, 'Braille', 'ccvm', 'value'),
3919     oils_i18n_gettext(589, 'Braille', 'ccvm', 'search_label')),
3920 (590, 'mr_hold_format', 'software', 
3921     oils_i18n_gettext(590, 'Software and video games', 'ccvm', 'value'),
3922     oils_i18n_gettext(590, 'Software and video games', 'ccvm', 'search_label')),
3923 (591, 'mr_hold_format', 'dvd', 
3924     oils_i18n_gettext(591, 'DVD', 'ccvm', 'value'),
3925     oils_i18n_gettext(591, 'DVD', 'ccvm', 'search_label')),
3926 (592, 'mr_hold_format', 'kit', 
3927     oils_i18n_gettext(592, 'Kit', 'ccvm', 'value'),
3928     oils_i18n_gettext(592, 'Kit', 'ccvm', 'search_label')),
3929 (593, 'mr_hold_format', 'map', 
3930     oils_i18n_gettext(593, 'Map', 'ccvm', 'value'),
3931     oils_i18n_gettext(593, 'Map', 'ccvm', 'search_label')),
3932 (594, 'mr_hold_format', 'microform', 
3933     oils_i18n_gettext(594, 'Microform', 'ccvm', 'value'),
3934     oils_i18n_gettext(594, 'Microform', 'ccvm', 'search_label')),
3935 (595, 'mr_hold_format', 'score', 
3936     oils_i18n_gettext(595, 'Music Score', 'ccvm', 'value'),
3937     oils_i18n_gettext(595, 'Music Score', 'ccvm', 'search_label')),
3938 (596, 'mr_hold_format', 'picture', 
3939     oils_i18n_gettext(596, 'Picture', 'ccvm', 'value'),
3940     oils_i18n_gettext(596, 'Picture', 'ccvm', 'search_label')),
3941 (597, 'mr_hold_format', 'equip', 
3942     oils_i18n_gettext(597, 'Equipment, games, toys', 'ccvm', 'value'),
3943     oils_i18n_gettext(597, 'Equipment, games, toys', 'ccvm', 'search_label')),
3944 (598, 'mr_hold_format', 'serial', 
3945     oils_i18n_gettext(598, 'Serials and magazines', 'ccvm', 'value'),
3946     oils_i18n_gettext(598, 'Serials and magazines', 'ccvm', 'search_label')),
3947 (599, 'mr_hold_format', 'vhs', 
3948     oils_i18n_gettext(599, 'VHS', 'ccvm', 'value'),
3949     oils_i18n_gettext(599, 'VHS', 'ccvm', 'search_label')),
3950 (600, 'mr_hold_format', 'cdaudiobook', 
3951     oils_i18n_gettext(600, 'CD Audiobook', 'ccvm', 'value'),
3952     oils_i18n_gettext(600, 'CD Audiobook', 'ccvm', 'search_label')),
3953 (601, 'mr_hold_format', 'cdmusic', 
3954     oils_i18n_gettext(601, 'CD Music recording', 'ccvm', 'value'),
3955     oils_i18n_gettext(601, 'CD Music recording', 'ccvm', 'search_label')),
3956 (602, 'mr_hold_format', 'casaudiobook', 
3957     oils_i18n_gettext(602, 'Cassette audiobook', 'ccvm', 'value'),
3958     oils_i18n_gettext(602, 'Cassette audiobook', 'ccvm', 'search_label')),
3959 (603, 'mr_hold_format', 'casmusic',
3960     oils_i18n_gettext(603, 'Audiocassette music recording', 'ccvm', 'value'),
3961     oils_i18n_gettext(603, 'Audiocassette music recording', 'ccvm', 'search_label')),
3962 (604, 'mr_hold_format', 'phonospoken', 
3963     oils_i18n_gettext(604, 'Phonograph spoken recording', 'ccvm', 'value'),
3964     oils_i18n_gettext(604, 'Phonograph spoken recording', 'ccvm', 'search_label')),
3965 (605, 'mr_hold_format', 'phonomusic', 
3966     oils_i18n_gettext(605, 'Phonograph music recording', 'ccvm', 'value'),
3967     oils_i18n_gettext(605, 'Phonograph music recording', 'ccvm', 'search_label')),
3968 (606, 'mr_hold_format', 'lpbook', 
3969     oils_i18n_gettext(606, 'Large Print Book', 'ccvm', 'value'),
3970     oils_i18n_gettext(606, 'Large Print Book', 'ccvm', 'search_label'))
3971 ;
3972
3973 -- but we can auto-generate the composite definitions
3974
3975 DO $$
3976     DECLARE format TEXT;
3977 BEGIN
3978     FOR format IN SELECT UNNEST(
3979         '{book,braille,software,dvd,kit,map,microform,score,picture,equip,serial,vhs,cdaudiobook,cdmusic,casaudiobook,casmusic,phonospoken,phonomusic,lpbook}'::text[]) LOOP
3980
3981         INSERT INTO config.composite_attr_entry_definition 
3982             (coded_value, definition) VALUES
3983             (
3984                 -- get the ID from the new ccvm above
3985                 (SELECT id FROM config.coded_value_map 
3986                     WHERE code = format AND ctype = 'mr_hold_format'),
3987                 -- get the def of the matching ccvm attached to the icon_format attr
3988                 (SELECT definition FROM config.composite_attr_entry_definition ccaed
3989                     JOIN config.coded_value_map ccvm ON (ccaed.coded_value = ccvm.id)
3990                     WHERE ccvm.ctype = 'icon_format' AND ccvm.code = format)
3991             );
3992     END LOOP; 
3993 END $$;
3994
3995 INSERT INTO config.coded_value_map
3996     (id, ctype, code, value, search_label) VALUES 
3997 (607, 'icon_format', 'music', 
3998     oils_i18n_gettext(607, 'Musical Sound Recording (Unknown Format)', 'ccvm', 'value'),
3999     oils_i18n_gettext(607, 'Musical Sound Recording (Unknown Format)', 'ccvm', 'search_label'));
4000
4001 INSERT INTO config.composite_attr_entry_definition 
4002     (coded_value, definition) VALUES
4003 (607, '{"0":{"_attr":"item_type","_val":"j"},"1":{"_not":[{"_attr":"sr_format","_val":"a"},{"_attr":"sr_format","_val":"b"},{"_attr":"sr_format","_val":"c"},{"_attr":"sr_format","_val":"d"},{"_attr":"sr_format","_val":"f"},{"_attr":"sr_format","_val":"e"},{"_attr":"sr_format","_val":"l"}]}}');
4004
4005 -- icon for blu-ray
4006 INSERT INTO config.coded_value_map
4007     (id, ctype, code, value, search_label) VALUES 
4008 (608, 'icon_format', 'blu-ray', 
4009     oils_i18n_gettext(608, 'Blu-ray', 'ccvm', 'value'),
4010     oils_i18n_gettext(608, 'Blu-ray', 'ccvm', 'search_label'));
4011 INSERT INTO config.composite_attr_entry_definition 
4012     (coded_value, definition) VALUES (608, '{"_attr":"vr_format","_val":"s"}');
4013
4014 -- metarecord hold format for blu-ray
4015 INSERT INTO config.coded_value_map
4016     (id, ctype, code, value, search_label) VALUES 
4017 (609, 'mr_hold_format', 'blu-ray', 
4018     oils_i18n_gettext(609, 'Blu-ray', 'ccvm', 'value'),
4019     oils_i18n_gettext(609, 'Blu-ray', 'ccvm', 'search_label'));
4020 INSERT INTO config.composite_attr_entry_definition 
4021     (coded_value, definition) VALUES (609, '{"_attr":"vr_format","_val":"s"}');
4022
4023
4024
4025 SELECT evergreen.upgrade_deps_block_check('0869', :eg_version);
4026
4027 CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity_update () RETURNS TRIGGER AS $f$
4028 BEGIN
4029     NEW.proximity := action.hold_copy_calculated_proximity(NEW.hold,NEW.target_copy);
4030     RETURN NEW;
4031 END;
4032 $f$ LANGUAGE PLPGSQL;
4033
4034 CREATE TRIGGER hold_copy_proximity_update_tgr BEFORE INSERT OR UPDATE ON action.hold_copy_map FOR EACH ROW EXECUTE PROCEDURE action.hold_copy_calculated_proximity_update ();
4035
4036 -- Now, cause the update we need in a HOT-friendly manner (http://pgsql.tapoueh.org/site/html/misc/hot.html)
4037 UPDATE action.hold_copy_map SET proximity = proximity WHERE proximity IS NULL;
4038
4039
4040 /*
4041  * Copyright (C) 2014  Equinox Software, Inc.
4042  * Mike Rylander <miker@esilibrary.com>
4043  *
4044  * This program is free software; you can redistribute it and/or
4045  * modify it under the terms of the GNU General Public License
4046  * as published by the Free Software Foundation; either version 2
4047  * of the License, or (at your option) any later version.
4048  *
4049  * This program is distributed in the hope that it will be useful,
4050  * but WITHOUT ANY WARRANTY; without even the implied warranty of
4051  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4052  * GNU General Public License for more details.
4053  *
4054  */
4055
4056
4057
4058 SELECT evergreen.upgrade_deps_block_check('0870', :eg_version);
4059
4060 CREATE OR REPLACE FUNCTION evergreen.located_uris (
4061     bibid BIGINT[],
4062     ouid INT,
4063     pref_lib INT DEFAULT NULL
4064 ) RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank INT) AS $$
4065     WITH all_orgs AS (SELECT COALESCE( enabled, FALSE ) AS flag FROM config.global_flag WHERE name = 'opac.located_uri.act_as_copy')
4066     SELECT DISTINCT ON (id) * FROM (
4067     SELECT acn.id, COALESCE(aou.name,aoud.name), acn.label_sortkey, evergreen.rank_ou(aou.id, $2, $3) AS pref_ou
4068       FROM asset.call_number acn
4069            INNER JOIN asset.uri_call_number_map auricnm ON acn.id = auricnm.call_number
4070            INNER JOIN asset.uri auri ON auri.id = auricnm.uri
4071            LEFT JOIN actor.org_unit_ancestors( COALESCE($3, $2) ) aou ON (acn.owning_lib = aou.id)
4072            LEFT JOIN actor.org_unit_descendants( COALESCE($3, $2) ) aoud ON (acn.owning_lib = aoud.id),
4073            all_orgs
4074       WHERE acn.record = ANY ($1)
4075           AND acn.deleted IS FALSE
4076           AND auri.active IS TRUE
4077           AND ((NOT all_orgs.flag AND aou.id IS NOT NULL) OR (all_orgs.flag AND COALESCE(aou.id,aoud.id) IS NOT NULL))
4078     UNION
4079     SELECT acn.id, COALESCE(aou.name,aoud.name) AS name, acn.label_sortkey, evergreen.rank_ou(aou.id, $2, $3) AS pref_ou
4080       FROM asset.call_number acn
4081            INNER JOIN asset.uri_call_number_map auricnm ON acn.id = auricnm.call_number
4082            INNER JOIN asset.uri auri ON auri.id = auricnm.uri
4083            LEFT JOIN actor.org_unit_ancestors( $2 ) aou ON (acn.owning_lib = aou.id)
4084            LEFT JOIN actor.org_unit_descendants( $2 ) aoud ON (acn.owning_lib = aoud.id),
4085            all_orgs
4086       WHERE acn.record = ANY ($1)
4087           AND acn.deleted IS FALSE
4088           AND auri.active IS TRUE
4089           AND ((NOT all_orgs.flag AND aou.id IS NOT NULL) OR (all_orgs.flag AND COALESCE(aou.id,aoud.id) IS NOT NULL)))x
4090     ORDER BY id, pref_ou DESC;
4091 $$
4092 LANGUAGE SQL STABLE;
4093
4094
4095
4096
4097 SELECT evergreen.upgrade_deps_block_check('0871', :eg_version);
4098
4099 INSERT INTO config.record_attr_definition 
4100     (name, label, multi, filter, composite) VALUES (
4101         'search_format', 
4102         oils_i18n_gettext('search_format', 'Search Formats', 'crad', 'label'),
4103         TRUE, TRUE, TRUE
4104     );
4105
4106 INSERT INTO config.coded_value_map
4107     (id, ctype, code, value, search_label) VALUES 
4108 (610, 'search_format', 'book', 
4109     oils_i18n_gettext(610, 'All Books', 'ccvm', 'value'),
4110     oils_i18n_gettext(610, 'All Books', 'ccvm', 'search_label')),
4111 (611, 'search_format', 'braille', 
4112     oils_i18n_gettext(611, 'Braille', 'ccvm', 'value'),
4113     oils_i18n_gettext(611, 'Braille', 'ccvm', 'search_label')),
4114 (612, 'search_format', 'software', 
4115     oils_i18n_gettext(612, 'Software and video games', 'ccvm', 'value'),
4116     oils_i18n_gettext(612, 'Software and video games', 'ccvm', 'search_label')),
4117 (613, 'search_format', 'dvd', 
4118     oils_i18n_gettext(613, 'DVD', 'ccvm', 'value'),
4119     oils_i18n_gettext(613, 'DVD', 'ccvm', 'search_label')),
4120 (614, 'search_format', 'ebook', 
4121     oils_i18n_gettext(614, 'E-book', 'ccvm', 'value'),
4122     oils_i18n_gettext(614, 'E-book', 'ccvm', 'search_label')),
4123 (615, 'search_format', 'eaudio', 
4124     oils_i18n_gettext(615, 'E-audio', 'ccvm', 'value'),
4125     oils_i18n_gettext(615, 'E-audio', 'ccvm', 'search_label')),
4126 (616, 'search_format', 'kit', 
4127     oils_i18n_gettext(616, 'Kit', 'ccvm', 'value'),
4128     oils_i18n_gettext(616, 'Kit', 'ccvm', 'search_label')),
4129 (617, 'search_format', 'map', 
4130     oils_i18n_gettext(617, 'Map', 'ccvm', 'value'),
4131     oils_i18n_gettext(617, 'Map', 'ccvm', 'search_label')),
4132 (618, 'search_format', 'microform', 
4133     oils_i18n_gettext(618, 'Microform', 'ccvm', 'value'),
4134     oils_i18n_gettext(618, 'Microform', 'ccvm', 'search_label')),
4135 (619, 'search_format', 'score', 
4136     oils_i18n_gettext(619, 'Music Score', 'ccvm', 'value'),
4137     oils_i18n_gettext(619, 'Music Score', 'ccvm', 'search_label')),
4138 (620, 'search_format', 'picture', 
4139     oils_i18n_gettext(620, 'Picture', 'ccvm', 'value'),
4140     oils_i18n_gettext(620, 'Picture', 'ccvm', 'search_label')),
4141 (621, 'search_format', 'equip', 
4142     oils_i18n_gettext(621, 'Equipment, games, toys', 'ccvm', 'value'),
4143     oils_i18n_gettext(621, 'Equipment, games, toys', 'ccvm', 'search_label')),
4144 (622, 'search_format', 'serial', 
4145     oils_i18n_gettext(622, 'Serials and magazines', 'ccvm', 'value'),
4146     oils_i18n_gettext(622, 'Serials and magazines', 'ccvm', 'search_label')),
4147 (623, 'search_format', 'vhs', 
4148     oils_i18n_gettext(623, 'VHS', 'ccvm', 'value'),
4149     oils_i18n_gettext(623, 'VHS', 'ccvm', 'search_label')),
4150 (624, 'search_format', 'evideo', 
4151     oils_i18n_gettext(624, 'E-video', 'ccvm', 'value'),
4152     oils_i18n_gettext(624, 'E-video', 'ccvm', 'search_label')),
4153 (625, 'search_format', 'cdaudiobook', 
4154     oils_i18n_gettext(625, 'CD Audiobook', 'ccvm', 'value'),
4155     oils_i18n_gettext(625, 'CD Audiobook', 'ccvm', 'search_label')),
4156 (626, 'search_format', 'cdmusic', 
4157     oils_i18n_gettext(626, 'CD Music recording', 'ccvm', 'value'),
4158     oils_i18n_gettext(626, 'CD Music recording', 'ccvm', 'search_label')),
4159 (627, 'search_format', 'casaudiobook', 
4160     oils_i18n_gettext(627, 'Cassette audiobook', 'ccvm', 'value'),
4161     oils_i18n_gettext(627, 'Cassette audiobook', 'ccvm', 'search_label')),
4162 (628, 'search_format', 'casmusic',
4163     oils_i18n_gettext(628, 'Audiocassette music recording', 'ccvm', 'value'),
4164     oils_i18n_gettext(628, 'Audiocassette music recording', 'ccvm', 'search_label')),
4165 (629, 'search_format', 'phonospoken', 
4166     oils_i18n_gettext(629, 'Phonograph spoken recording', 'ccvm', 'value'),
4167     oils_i18n_gettext(629, 'Phonograph spoken recording', 'ccvm', 'search_label')),
4168 (630, 'search_format', 'phonomusic', 
4169     oils_i18n_gettext(630, 'Phonograph music recording', 'ccvm', 'value'),
4170     oils_i18n_gettext(630, 'Phonograph music recording', 'ccvm', 'search_label')),
4171 (631, 'search_format', 'lpbook', 
4172     oils_i18n_gettext(631, 'Large Print Book', 'ccvm', 'value'),
4173     oils_i18n_gettext(631, 'Large Print Book', 'ccvm', 'search_label')),
4174 (632, 'search_format', 'music', 
4175     oils_i18n_gettext(632, 'All Music', 'ccvm', 'label'),
4176     oils_i18n_gettext(632, 'All Music', 'ccvm', 'search_label')),
4177 (633, 'search_format', 'blu-ray', 
4178     oils_i18n_gettext(633, 'Blu-ray', 'ccvm', 'value'),
4179     oils_i18n_gettext(633, 'Blu-ray', 'ccvm', 'search_label'));
4180
4181
4182
4183 -- copy the composite definition from icon_format into 
4184 -- search_format for a baseline data set
4185 DO $$
4186     DECLARE format config.coded_value_map%ROWTYPE;
4187 BEGIN
4188     FOR format IN SELECT * 
4189         FROM config.coded_value_map WHERE ctype = 'icon_format'
4190     LOOP
4191         INSERT INTO config.composite_attr_entry_definition 
4192             (coded_value, definition) VALUES
4193             (
4194                 -- get the ID from the new ccvm above
4195                 (SELECT id FROM config.coded_value_map 
4196                     WHERE code = format.code AND ctype = 'search_format'),
4197
4198                 -- def of the matching icon_format attr
4199                 (SELECT definition FROM config.composite_attr_entry_definition 
4200                     WHERE coded_value = format.id)
4201             );
4202     END LOOP; 
4203 END $$;
4204
4205 -- modify the 'book' definition so that it includes large print
4206 UPDATE config.composite_attr_entry_definition 
4207     SET definition = '{"0":[{"_attr":"item_type","_val":"a"},{"_attr":"item_type","_val":"t"}],"1":{"_not":[{"_attr":"item_form","_val":"a"},{"_attr":"item_form","_val":"b"},{"_attr":"item_form","_val":"c"},{"_attr":"item_form","_val":"f"},{"_attr":"item_form","_val":"o"},{"_attr":"item_form","_val":"q"},{"_attr":"item_form","_val":"r"},{"_attr":"item_form","_val":"s"}]},"2":[{"_attr":"bib_level","_val":"a"},{"_attr":"bib_level","_val":"c"},{"_attr":"bib_level","_val":"d"},{"_attr":"bib_level","_val":"m"}]}'
4208     WHERE coded_value = 610;
4209
4210 -- modify 'music' to include all recorded music, regardless of format
4211 UPDATE config.composite_attr_entry_definition 
4212     SET definition = '{"_attr":"item_type","_val":"j"}'
4213     WHERE coded_value = 632;
4214
4215 UPDATE config.global_flag 
4216     SET value = 'search_format' 
4217     WHERE name = 'opac.format_selector.attr';
4218
4219
4220
4221 SELECT evergreen.upgrade_deps_block_check('0872', :eg_version);
4222
4223 CREATE OR REPLACE FUNCTION metabib.remap_metarecord_for_bib( bib_id BIGINT, fp TEXT, bib_is_deleted BOOL DEFAULT FALSE, retain_deleted BOOL DEFAULT FALSE ) RETURNS BIGINT AS $func$
4224 DECLARE
4225     new_mapping     BOOL := TRUE;
4226     source_count    INT;
4227     old_mr          BIGINT;
4228     tmp_mr          metabib.metarecord%ROWTYPE;
4229     deleted_mrs     BIGINT[];
4230 BEGIN
4231
4232     -- We need to make sure we're not a deleted master record of an MR
4233     IF bib_is_deleted THEN
4234         FOR old_mr IN SELECT id FROM metabib.metarecord WHERE master_record = bib_id LOOP
4235
4236             IF NOT retain_deleted THEN -- Go away for any MR that we're master of, unless retained
4237                 DELETE FROM metabib.metarecord_source_map WHERE source = bib_id;
4238             END IF;
4239
4240             -- Now, are there any more sources on this MR?
4241             SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = old_mr;
4242
4243             IF source_count = 0 AND NOT retain_deleted THEN -- No other records
4244                 deleted_mrs := ARRAY_APPEND(deleted_mrs, old_mr); -- Just in case...
4245                 DELETE FROM metabib.metarecord WHERE id = old_mr;
4246
4247             ELSE -- indeed there are. Update it with a null cache and recalcualated master record
4248                 UPDATE  metabib.metarecord
4249                   SET   mods = NULL,
4250                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
4251                   WHERE id = old_mr;
4252             END IF;
4253         END LOOP;
4254
4255     ELSE -- insert or update
4256
4257         FOR tmp_mr IN SELECT m.* FROM metabib.metarecord m JOIN metabib.metarecord_source_map s ON (s.metarecord = m.id) WHERE s.source = bib_id LOOP
4258
4259             -- Find the first fingerprint-matching
4260             IF old_mr IS NULL AND fp = tmp_mr.fingerprint THEN
4261                 old_mr := tmp_mr.id;
4262                 new_mapping := FALSE;
4263
4264             ELSE -- Our fingerprint changed ... maybe remove the old MR
4265                 DELETE FROM metabib.metarecord_source_map WHERE metarecord = old_mr AND source = bib_id; -- remove the old source mapping
4266                 SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id;
4267                 IF source_count = 0 THEN -- No other records
4268                     deleted_mrs := ARRAY_APPEND(deleted_mrs, tmp_mr.id);
4269                     DELETE FROM metabib.metarecord WHERE id = tmp_mr.id;
4270                 END IF;
4271             END IF;
4272
4273         END LOOP;
4274
4275         -- we found no suitable, preexisting MR based on old source maps
4276         IF old_mr IS NULL THEN
4277             SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp; -- is there one for our current fingerprint?
4278
4279             IF old_mr IS NULL THEN -- nope, create one and grab its id
4280                 INSERT INTO metabib.metarecord ( fingerprint, master_record ) VALUES ( fp, bib_id );
4281                 SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp;
4282
4283             ELSE -- indeed there is. update it with a null cache and recalcualated master record
4284                 UPDATE  metabib.metarecord
4285                   SET   mods = NULL,
4286                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
4287                   WHERE id = old_mr;
4288             END IF;
4289
4290         ELSE -- there was one we already attached to, update its mods cache and master_record
4291             UPDATE  metabib.metarecord
4292               SET   mods = NULL,
4293                     master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
4294               WHERE id = old_mr;
4295         END IF;
4296
4297         IF new_mapping THEN
4298             INSERT INTO metabib.metarecord_source_map (metarecord, source) VALUES (old_mr, bib_id); -- new source mapping
4299         END IF;
4300
4301     END IF;
4302
4303     IF ARRAY_UPPER(deleted_mrs,1) > 0 THEN
4304         UPDATE action.hold_request SET target = old_mr WHERE target IN ( SELECT unnest(deleted_mrs) ) AND hold_type = 'M'; -- if we had to delete any MRs above, make sure their holds are moved
4305     END IF;
4306
4307     RETURN old_mr;
4308
4309 END;
4310 $func$ LANGUAGE PLPGSQL;
4311
4312 DROP FUNCTION metabib.remap_metarecord_for_bib( bib_id BIGINT, fp TEXT );
4313
4314 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
4315 DECLARE
4316     tmp_bool BOOL;
4317 BEGIN
4318
4319     IF NEW.deleted THEN -- If this bib is deleted
4320
4321         PERFORM * FROM config.internal_flag WHERE
4322             name = 'ingest.metarecord_mapping.preserve_on_delete' AND enabled;
4323
4324         tmp_bool := FOUND; -- Just in case this is changed by some other statement
4325
4326         PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint, TRUE, tmp_bool );
4327
4328         IF NOT tmp_bool THEN
4329             -- One needs to keep these around to support searches
4330             -- with the #deleted modifier, so one should turn on the named
4331             -- internal flag for that functionality.
4332             DELETE FROM metabib.record_attr_vector_list WHERE source = NEW.id;
4333         END IF;
4334
4335         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
4336         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
4337         DELETE FROM metabib.browse_entry_def_map WHERE source = NEW.id; -- Don't auto-suggest deleted bibs
4338         RETURN NEW; -- and we're done
4339     END IF;
4340
4341     IF TG_OP = 'UPDATE' THEN -- re-ingest?
4342         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
4343
4344         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
4345             RETURN NEW;
4346         END IF;
4347     END IF;
4348
4349     -- Record authority linking
4350     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
4351     IF NOT FOUND THEN
4352         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
4353     END IF;
4354
4355     -- Flatten and insert the mfr data
4356     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
4357     IF NOT FOUND THEN
4358         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
4359
4360         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
4361         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
4362         IF NOT FOUND THEN
4363             PERFORM metabib.reingest_record_attributes(NEW.id, NULL, NEW.marc, TG_OP = 'INSERT' OR OLD.deleted);
4364         END IF;
4365     END IF;
4366
4367     -- Gather and insert the field entry data
4368     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
4369
4370     -- Located URI magic
4371     IF TG_OP = 'INSERT' THEN
4372         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4373         IF NOT FOUND THEN
4374             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4375         END IF;
4376     ELSE
4377         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4378         IF NOT FOUND THEN
4379             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4380         END IF;
4381     END IF;
4382
4383     -- (re)map metarecord-bib linking
4384     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
4385         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
4386         IF NOT FOUND THEN
4387             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4388         END IF;
4389     ELSE -- we're doing an update, and we're not deleted, remap
4390         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
4391         IF NOT FOUND THEN
4392             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4393         END IF;
4394     END IF;
4395
4396     RETURN NEW;
4397 END;
4398 $func$ LANGUAGE PLPGSQL;
4399
4400 CREATE OR REPLACE FUNCTION unapi.mmr (
4401     obj_id BIGINT,
4402     format TEXT,
4403     ename TEXT,
4404     includes TEXT[],
4405     org TEXT,
4406     depth INT DEFAULT NULL,
4407     slimit HSTORE DEFAULT NULL,
4408     soffset HSTORE DEFAULT NULL,
4409     include_xmlns BOOL DEFAULT TRUE,
4410     pref_lib INT DEFAULT NULL
4411 )
4412 RETURNS XML AS $F$
4413 DECLARE
4414     mmrec   metabib.metarecord%ROWTYPE;
4415     leadrec biblio.record_entry%ROWTYPE;
4416     subrec biblio.record_entry%ROWTYPE;
4417     layout  unapi.bre_output_layout%ROWTYPE;
4418     xfrm    config.xml_transform%ROWTYPE;
4419     ouid    INT;
4420     xml_buf TEXT; -- growing XML document
4421     tmp_xml TEXT; -- single-use XML string
4422     xml_frag TEXT; -- single-use XML fragment
4423     top_el  TEXT;
4424     output  XML;
4425     hxml    XML;
4426     axml    XML;
4427     subxml  XML; -- subordinate records elements
4428     sub_xpath TEXT; 
4429     parts   TEXT[]; 
4430 BEGIN
4431
4432     -- xpath for extracting bre.marc values from subordinate records 
4433     -- so they may be appended to the MARC of the master record prior
4434     -- to XSLT processing.
4435     -- subjects, isbn, issn, upc -- anything else?
4436     sub_xpath := 
4437       '//*[starts-with(@tag, "6") or @tag="020" or @tag="022" or @tag="024"]';
4438
4439     IF org = '-' OR org IS NULL THEN
4440         SELECT shortname INTO org FROM evergreen.org_top();
4441     END IF;
4442
4443     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
4444
4445     IF ouid IS NULL THEN
4446         RETURN NULL::XML;
4447     END IF;
4448
4449     SELECT INTO mmrec * FROM metabib.metarecord WHERE id = obj_id;
4450     IF NOT FOUND THEN
4451         RETURN NULL::XML;
4452     END IF;
4453
4454     -- TODO: aggregate holdings from constituent records
4455     IF format = 'holdings_xml' THEN -- the special case
4456         output := unapi.mmr_holdings_xml(
4457             obj_id, ouid, org, depth,
4458             evergreen.array_remove_item_by_value(includes,'holdings_xml'),
4459             slimit, soffset, include_xmlns, pref_lib);
4460         RETURN output;
4461     END IF;
4462
4463     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
4464
4465     IF layout.name IS NULL THEN
4466         RETURN NULL::XML;
4467     END IF;
4468
4469     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
4470
4471     SELECT INTO leadrec * FROM biblio.record_entry WHERE id = mmrec.master_record;
4472
4473     -- Grab distinct MVF for all records if requested
4474     IF ('mra' = ANY (includes)) THEN 
4475         axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,org,depth,NULL,NULL,TRUE,pref_lib);
4476     ELSE
4477         axml := NULL::XML;
4478     END IF;
4479
4480     xml_buf = leadrec.marc;
4481
4482     hxml := NULL::XML;
4483     IF ('holdings_xml' = ANY (includes)) THEN
4484         hxml := unapi.mmr_holdings_xml(
4485                     obj_id, ouid, org, depth,
4486                     evergreen.array_remove_item_by_value(includes,'holdings_xml'),
4487                     slimit, soffset, include_xmlns, pref_lib);
4488     END IF;
4489
4490     subxml := NULL::XML;
4491     parts := '{}'::TEXT[];
4492     FOR subrec IN SELECT bre.* FROM biblio.record_entry bre
4493          JOIN metabib.metarecord_source_map mmsm ON (mmsm.source = bre.id)
4494          JOIN metabib.metarecord mmr ON (mmr.id = mmsm.metarecord)
4495          WHERE mmr.id = obj_id AND NOT bre.deleted
4496          ORDER BY CASE WHEN bre.id = mmr.master_record THEN 0 ELSE bre.id END
4497          LIMIT COALESCE((slimit->'bre')::INT, 5) LOOP
4498
4499         IF subrec.id = leadrec.id THEN CONTINUE; END IF;
4500         -- Append choice data from the the non-lead records to the 
4501         -- the lead record document
4502
4503         parts := parts || xpath(sub_xpath, subrec.marc::XML)::TEXT[];
4504     END LOOP;
4505
4506     SELECT ARRAY_TO_STRING( ARRAY_AGG( DISTINCT p ), '' )::XML INTO subxml FROM UNNEST(parts) p;
4507
4508     -- append data from the subordinate records to the 
4509     -- main record document before applying the XSLT
4510
4511     IF subxml IS NOT NULL THEN 
4512         xml_buf := REGEXP_REPLACE(xml_buf, 
4513             '</record>(.*?)$', subxml || '</record>' || E'\\1');
4514     END IF;
4515
4516     IF format = 'marcxml' THEN
4517          -- If we're not using the prefixed namespace in 
4518          -- this record, then remove all declarations of it
4519         IF xml_buf !~ E'<marc:' THEN
4520            xml_buf := REGEXP_REPLACE(xml_buf, 
4521             ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
4522         END IF; 
4523     ELSE
4524         xml_buf := oils_xslt_process(xml_buf, xfrm.xslt)::XML;
4525     END IF;
4526
4527     -- update top_el to reflect the change in xml_buf, which may
4528     -- now be a different type of document (e.g. record -> mods)
4529     top_el := REGEXP_REPLACE(xml_buf, E'^.*?<((?:\\S+:)?' || 
4530         layout.holdings_element || ').*$', E'\\1');
4531
4532     IF axml IS NOT NULL THEN 
4533         xml_buf := REGEXP_REPLACE(xml_buf, 
4534             '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
4535     END IF;
4536
4537     IF hxml IS NOT NULL THEN
4538         xml_buf := REGEXP_REPLACE(xml_buf, 
4539             '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
4540     END IF;
4541
4542     IF ('mmr.unapi' = ANY (includes)) THEN 
4543         output := REGEXP_REPLACE(
4544             xml_buf,
4545             '</' || top_el || '>(.*?)',
4546             XMLELEMENT(
4547                 name abbr,
4548                 XMLATTRIBUTES(
4549                     'http://www.w3.org/1999/xhtml' AS xmlns,
4550                     'unapi-id' AS class,
4551                     'tag:open-ils.org:U2@mmr/' || obj_id || '/' || org AS title
4552                 )
4553             )::TEXT || '</' || top_el || E'>\\1'
4554         );
4555     ELSE
4556         output := xml_buf;
4557     END IF;
4558
4559     -- remove ignorable whitesace
4560     output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
4561     RETURN output;
4562 END;
4563 $F$ LANGUAGE PLPGSQL STABLE;
4564
4565 -- Forcibly remap deleted master records, retaining the linkage if so configured.
4566 SELECT  count(metabib.remap_metarecord_for_bib( bre.id, bre.fingerprint, TRUE, COALESCE(flag.enabled,FALSE)))
4567   FROM  metabib.metarecord metar
4568         JOIN biblio.record_entry bre ON bre.id = metar.master_record,
4569         config.internal_flag flag
4570   WHERE bre.deleted = TRUE AND flag.name = 'ingest.metarecord_mapping.preserve_on_delete';
4571
4572
4573
4574 SELECT evergreen.upgrade_deps_block_check('0873', :eg_version);
4575
4576 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
4577   RETURNS integer AS
4578 $func$
4579 DECLARE
4580     requestor_object    actor.usr%ROWTYPE;
4581     user_object         actor.usr%ROWTYPE;
4582     item_object         asset.copy%ROWTYPE;
4583     item_cn_object      asset.call_number%ROWTYPE;
4584     my_item_age         INTERVAL;
4585     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
4586     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
4587     weights             config.hold_matrix_weights%ROWTYPE;
4588     denominator         NUMERIC(6,2);
4589     v_pickup_ou         ALIAS FOR pickup_ou;
4590     v_request_ou         ALIAS FOR request_ou;
4591 BEGIN
4592     SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
4593     SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
4594     SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
4595     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
4596     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
4597
4598     SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
4599
4600     -- The item's owner should probably be the one determining if the item is holdable
4601     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
4602     -- This flag will allow for setting it to the owning library (where the call number "lives")
4603     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
4604
4605     -- Grab the closest set circ weight setting.
4606     IF NOT FOUND THEN
4607         -- Default to circ library
4608         SELECT INTO weights hw.*
4609           FROM config.weight_assoc wa
4610                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
4611                JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
4612           WHERE active
4613           ORDER BY d.distance
4614           LIMIT 1;
4615     ELSE
4616         -- Flag is set, use owning library
4617         SELECT INTO weights hw.*
4618           FROM config.weight_assoc wa
4619                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
4620                JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) d ON (wa.org_unit = d.id)
4621           WHERE active
4622           ORDER BY d.distance
4623           LIMIT 1;
4624     END IF;
4625
4626     -- No weights? Bad admin! Defaults to handle that anyway.
4627     IF weights.id IS NULL THEN
4628         weights.user_home_ou    := 5.0;
4629         weights.request_ou      := 5.0;
4630         weights.pickup_ou       := 5.0;
4631         weights.item_owning_ou  := 5.0;
4632         weights.item_circ_ou    := 5.0;
4633         weights.usr_grp         := 7.0;
4634         weights.requestor_grp   := 8.0;
4635         weights.circ_modifier   := 4.0;
4636         weights.marc_type       := 3.0;
4637         weights.marc_form       := 2.0;
4638         weights.marc_bib_level  := 1.0;
4639         weights.marc_vr_format  := 1.0;
4640         weights.juvenile_flag   := 4.0;
4641         weights.ref_flag        := 0.0;
4642         weights.item_age        := 0.0;
4643     END IF;
4644
4645     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
4646     -- If you break your org tree with funky parenting this may be wrong
4647     -- 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
4648     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
4649     WITH all_distance(distance) AS (
4650             SELECT depth AS distance FROM actor.org_unit_type
4651         UNION
4652             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
4653         )
4654     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
4655
4656     -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
4657     -- This may be better implemented as part of the upgrade script?
4658     -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
4659     -- Then remove this flag, of course.
4660     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
4661
4662     IF FOUND THEN
4663         -- Note: This, to me, is REALLY hacky. I put it in anyway.
4664         -- If you can't tell, this is a single call swap on two variables.
4665         SELECT INTO user_object.profile, requestor_object.profile
4666                     requestor_object.profile, user_object.profile;
4667     END IF;
4668
4669     -- Select the winning matchpoint into the matchpoint variable for returning
4670     SELECT INTO matchpoint m.*
4671       FROM  config.hold_matrix_matchpoint m
4672             /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
4673             LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
4674             LEFT JOIN actor.org_unit_ancestors_distance( v_pickup_ou ) puoua ON m.pickup_ou = puoua.id
4675             LEFT JOIN actor.org_unit_ancestors_distance( v_request_ou ) rqoua ON m.request_ou = rqoua.id
4676             LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
4677             LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
4678             LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
4679       WHERE m.active
4680             -- Permission Groups
4681          -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
4682             AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
4683             -- Org Units
4684             AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
4685             AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
4686             AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
4687             AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
4688             AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
4689             -- Static User Checks
4690             AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
4691             -- Static Item Checks
4692             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
4693             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
4694             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
4695             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
4696             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
4697             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
4698             AND (m.item_age             IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
4699       ORDER BY
4700             -- Permission Groups
4701             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
4702             CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
4703             -- Org Units
4704             CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
4705             CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
4706             CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
4707             CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
4708             CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
4709             -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
4710             CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
4711             -- Static Item Checks
4712             CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
4713             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
4714             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
4715             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
4716             CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
4717             -- Item age has a slight adjustment to weight based on value.
4718             -- This should ensure that a shorter age limit comes first when all else is equal.
4719             -- NOTE: This assumes that intervals will normally be in days.
4720             CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
4721             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
4722             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
4723             m.id;
4724
4725     -- Return just the ID for now
4726     RETURN matchpoint.id;
4727 END;
4728 $func$ LANGUAGE 'plpgsql';
4729
4730 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$
4731 DECLARE
4732     matchpoint_id        INT;
4733     user_object        actor.usr%ROWTYPE;
4734     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
4735     standing_penalty    config.standing_penalty%ROWTYPE;
4736     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
4737     transit_source        actor.org_unit%ROWTYPE;
4738     item_object        asset.copy%ROWTYPE;
4739     item_cn_object     asset.call_number%ROWTYPE;
4740     item_status_object  config.copy_status%ROWTYPE;
4741     item_location_object    asset.copy_location%ROWTYPE;
4742     ou_skip              actor.org_unit_setting%ROWTYPE;
4743     result            action.matrix_test_result;
4744     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
4745     use_active_date   TEXT;
4746     age_protect_date  TIMESTAMP WITH TIME ZONE;
4747     hold_count        INT;
4748     hold_transit_prox    INT;
4749     frozen_hold_count    INT;
4750     context_org_list    INT[];
4751     done            BOOL := FALSE;
4752     hold_penalty TEXT;
4753     v_pickup_ou ALIAS FOR pickup_ou;
4754     v_request_ou ALIAS FOR request_ou;
4755 BEGIN
4756     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
4757     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( v_pickup_ou );
4758
4759     result.success := TRUE;
4760
4761     -- The HOLD penalty block only applies to new holds.
4762     -- The CAPTURE penalty block applies to existing holds.
4763     hold_penalty := 'HOLD';
4764     IF retargetting THEN
4765         hold_penalty := 'CAPTURE';
4766     END IF;
4767
4768     -- Fail if we couldn't find a user
4769     IF user_object.id IS NULL THEN
4770         result.fail_part := 'no_user';
4771         result.success := FALSE;
4772         done := TRUE;
4773         RETURN NEXT result;
4774         RETURN;
4775     END IF;
4776
4777     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
4778
4779     -- Fail if we couldn't find a copy
4780     IF item_object.id IS NULL THEN
4781         result.fail_part := 'no_item';
4782         result.success := FALSE;
4783         done := TRUE;
4784         RETURN NEXT result;
4785         RETURN;
4786     END IF;
4787
4788     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(v_pickup_ou, v_request_ou, match_item, match_user, match_requestor);
4789     result.matchpoint := matchpoint_id;
4790
4791     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
4792
4793     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
4794     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
4795         result.fail_part := 'circ.holds.target_skip_me';
4796         result.success := FALSE;
4797         done := TRUE;
4798         RETURN NEXT result;
4799         RETURN;
4800     END IF;
4801
4802     -- Fail if user is barred
4803     IF user_object.barred IS TRUE THEN
4804         result.fail_part := 'actor.usr.barred';
4805         result.success := FALSE;
4806         done := TRUE;
4807         RETURN NEXT result;
4808         RETURN;
4809     END IF;
4810
4811     SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
4812     SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
4813     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
4814
4815     -- Fail if we couldn't find any matchpoint (requires a default)
4816     IF matchpoint_id IS NULL THEN
4817         result.fail_part := 'no_matchpoint';
4818         result.success := FALSE;
4819         done := TRUE;
4820         RETURN NEXT result;
4821         RETURN;
4822     END IF;
4823
4824     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
4825
4826     IF hold_test.holdable IS FALSE THEN
4827         result.fail_part := 'config.hold_matrix_test.holdable';
4828         result.success := FALSE;
4829         done := TRUE;
4830         RETURN NEXT result;
4831     END IF;
4832
4833     IF item_object.holdable IS FALSE THEN
4834         result.fail_part := 'item.holdable';
4835         result.success := FALSE;
4836         done := TRUE;
4837         RETURN NEXT result;
4838     END IF;
4839
4840     IF item_status_object.holdable IS FALSE THEN
4841         result.fail_part := 'status.holdable';
4842         result.success := FALSE;
4843         done := TRUE;
4844         RETURN NEXT result;
4845     END IF;
4846
4847     IF item_location_object.holdable IS FALSE THEN
4848         result.fail_part := 'location.holdable';
4849         result.success := FALSE;
4850         done := TRUE;
4851         RETURN NEXT result;
4852     END IF;
4853
4854     IF hold_test.transit_range IS NOT NULL THEN
4855         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
4856         IF hold_test.distance_is_from_owner THEN
4857             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;
4858         ELSE
4859             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
4860         END IF;
4861
4862         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = v_pickup_ou;
4863
4864         IF NOT FOUND THEN
4865             result.fail_part := 'transit_range';
4866             result.success := FALSE;
4867             done := TRUE;
4868             RETURN NEXT result;
4869         END IF;
4870     END IF;
4871  
4872     FOR standing_penalty IN
4873         SELECT  DISTINCT csp.*
4874           FROM  actor.usr_standing_penalty usp
4875                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
4876           WHERE usr = match_user
4877                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
4878                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
4879                 AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
4880
4881         result.fail_part := standing_penalty.name;
4882         result.success := FALSE;
4883         done := TRUE;
4884         RETURN NEXT result;
4885     END LOOP;
4886
4887     IF hold_test.stop_blocked_user IS TRUE THEN
4888         FOR standing_penalty IN
4889             SELECT  DISTINCT csp.*
4890               FROM  actor.usr_standing_penalty usp
4891                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
4892               WHERE usr = match_user
4893                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
4894                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
4895                     AND csp.block_list LIKE '%CIRC%' LOOP
4896     
4897             result.fail_part := standing_penalty.name;
4898             result.success := FALSE;
4899             done := TRUE;
4900             RETURN NEXT result;
4901         END LOOP;
4902     END IF;
4903
4904     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
4905         SELECT    INTO hold_count COUNT(*)
4906           FROM    action.hold_request
4907           WHERE    usr = match_user
4908             AND fulfillment_time IS NULL
4909             AND cancel_time IS NULL
4910             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
4911
4912         IF hold_count >= hold_test.max_holds THEN
4913             result.fail_part := 'config.hold_matrix_test.max_holds';
4914             result.success := FALSE;
4915             done := TRUE;
4916             RETURN NEXT result;
4917         END IF;
4918     END IF;
4919
4920     IF item_object.age_protect IS NOT NULL THEN
4921         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
4922         IF hold_test.distance_is_from_owner THEN
4923             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
4924         ELSE
4925             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
4926         END IF;
4927         IF use_active_date = 'true' THEN
4928             age_protect_date := COALESCE(item_object.active_date, NOW());
4929         ELSE
4930             age_protect_date := item_object.create_date;
4931         END IF;
4932         IF age_protect_date + age_protect_object.age > NOW() THEN
4933             IF hold_test.distance_is_from_owner THEN
4934                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
4935                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = v_pickup_ou;
4936             ELSE
4937                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = v_pickup_ou;
4938             END IF;
4939
4940             IF hold_transit_prox > age_protect_object.prox THEN
4941                 result.fail_part := 'config.rule_age_hold_protect.prox';
4942                 result.success := FALSE;
4943                 done := TRUE;
4944                 RETURN NEXT result;
4945             END IF;
4946         END IF;
4947     END IF;
4948
4949     IF NOT done THEN
4950         RETURN NEXT result;
4951     END IF;
4952
4953     RETURN;
4954 END;
4955 $func$ LANGUAGE plpgsql;
4956
4957
4958
4959 SELECT evergreen.upgrade_deps_block_check('0874', :eg_version);
4960
4961 DROP FUNCTION IF EXISTS evergreen.oils_xpath( TEXT, TEXT, ANYARRAY);
4962 DROP FUNCTION IF EXISTS public.oils_xpath(TEXT, TEXT, ANYARRAY);
4963 DROP FUNCTION IF EXISTS public.oils_xpath(TEXT, TEXT);
4964 DROP FUNCTION IF EXISTS public.oils_xslt_process(TEXT, TEXT);
4965
4966 CREATE OR REPLACE FUNCTION evergreen.xml_famous5_to_text( TEXT ) RETURNS TEXT AS $f$
4967  SELECT REPLACE(
4968             REPLACE(
4969                 REPLACE(
4970                     REPLACE(
4971                         REPLACE( $1, '&lt;', '<'),
4972                         '&gt;',
4973                         '>'
4974                     ),
4975                     '&apos;',
4976                     $$'$$
4977                 ), -- ' ... vim
4978                 '&quot;',
4979                 '"'
4980             ),
4981             '&amp;',
4982             '&'
4983         );
4984 $f$ LANGUAGE SQL IMMUTABLE;
4985
4986 CREATE OR REPLACE FUNCTION evergreen.oils_xpath ( TEXT, TEXT, TEXT[] ) RETURNS TEXT[] AS $f$
4987     SELECT  ARRAY_AGG(
4988                 CASE WHEN strpos(x,'<') = 1 THEN -- It's an element node
4989                     x
4990                 ELSE -- it's text-ish
4991                     evergreen.xml_famous5_to_text(x)
4992                 END
4993             )
4994       FROM  UNNEST(XPATH( $1, $2::XML, $3 )::TEXT[]) x;
4995 $f$ LANGUAGE SQL IMMUTABLE;
4996
4997 -- Trust me, it's just simpler to duplicate these...
4998 CREATE OR REPLACE FUNCTION evergreen.oils_xpath ( TEXT, TEXT ) RETURNS TEXT[] AS $f$
4999     SELECT  ARRAY_AGG(
5000                 CASE WHEN strpos(x,'<') = 1 THEN -- It's an element node
5001                     x
5002                 ELSE -- it's text-ish
5003                     evergreen.xml_famous5_to_text(x)
5004                 END
5005             )
5006       FROM  UNNEST(XPATH( $1, $2::XML)::TEXT[]) x;
5007 $f$ LANGUAGE SQL IMMUTABLE;
5008
5009 CREATE OR REPLACE FUNCTION evergreen.oils_xslt_process(TEXT, TEXT) RETURNS TEXT AS $func$
5010   use strict;
5011
5012   use XML::LibXSLT;
5013   use XML::LibXML;
5014
5015   my $doc = shift;
5016   my $xslt = shift;
5017
5018   # The following approach uses the older XML::LibXML 1.69 / XML::LibXSLT 1.68
5019   # methods of parsing XML documents and stylesheets, in the hopes of broader
5020   # compatibility with distributions
5021   my $parser = $_SHARED{'_xslt_process'}{parsers}{xml} || XML::LibXML->new();
5022
5023   # Cache the XML parser, if we do not already have one
5024   $_SHARED{'_xslt_process'}{parsers}{xml} = $parser
5025     unless ($_SHARED{'_xslt_process'}{parsers}{xml});
5026
5027   my $xslt_parser = $_SHARED{'_xslt_process'}{parsers}{xslt} || XML::LibXSLT->new();
5028
5029   # Cache the XSLT processor, if we do not already have one
5030   $_SHARED{'_xslt_process'}{parsers}{xslt} = $xslt_parser
5031     unless ($_SHARED{'_xslt_process'}{parsers}{xslt});
5032
5033   my $stylesheet = $_SHARED{'_xslt_process'}{stylesheets}{$xslt} ||
5034     $xslt_parser->parse_stylesheet( $parser->parse_string($xslt) );
5035
5036   $_SHARED{'_xslt_process'}{stylesheets}{$xslt} = $stylesheet
5037     unless ($_SHARED{'_xslt_process'}{stylesheets}{$xslt});
5038
5039   return $stylesheet->output_string(
5040     $stylesheet->transform(
5041       $parser->parse_string($doc)
5042     )
5043   );
5044
5045 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
5046
5047 CREATE OR REPLACE FUNCTION authority.simple_heading_set( marcxml TEXT ) RETURNS SETOF authority.simple_heading AS $func$
5048 DECLARE
5049     res             authority.simple_heading%ROWTYPE;
5050     acsaf           authority.control_set_authority_field%ROWTYPE;
5051     tag_used        TEXT;
5052     nfi_used        TEXT;
5053     sf              TEXT;
5054     cset            INT;
5055     heading_text    TEXT;
5056     joiner_text     TEXT;
5057     sort_text       TEXT;
5058     tmp_text        TEXT;
5059     tmp_xml         TEXT;
5060     first_sf        BOOL;
5061     auth_id         INT DEFAULT COALESCE(NULLIF(oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml), ''), '0')::INT; 
5062 BEGIN
5063
5064     SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
5065
5066     IF cset IS NULL THEN
5067         SELECT  control_set INTO cset
5068           FROM  authority.control_set_authority_field
5069           WHERE tag IN ( SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]))
5070           LIMIT 1;
5071     END IF;
5072
5073     res.record := auth_id;
5074
5075     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
5076
5077         res.atag := acsaf.id;
5078         tag_used := acsaf.tag;
5079         nfi_used := acsaf.nfi;
5080         joiner_text := COALESCE(acsaf.joiner, ' ');
5081
5082         FOR tmp_xml IN SELECT UNNEST(XPATH('//*[@tag="'||tag_used||'"]', marcxml::XML)::TEXT[]) LOOP
5083
5084             heading_text := COALESCE(
5085                 oils_xpath_string('./*[contains("'||acsaf.display_sf_list||'",@code)]', tmp_xml, joiner_text),
5086                 ''
5087             );
5088
5089             IF nfi_used IS NOT NULL THEN
5090
5091                 sort_text := SUBSTRING(
5092                     heading_text FROM
5093                     COALESCE(
5094                         NULLIF(
5095                             REGEXP_REPLACE(
5096                                 oils_xpath_string('./@ind'||nfi_used, tmp_xml::TEXT),
5097                                 $$\D+$$,
5098                                 '',
5099                                 'g'
5100                             ),
5101                             ''
5102                         )::INT,
5103                         0
5104                     ) + 1
5105                 );
5106
5107             ELSE
5108                 sort_text := heading_text;
5109             END IF;
5110
5111             IF heading_text IS NOT NULL AND heading_text <> '' THEN
5112                 res.value := heading_text;
5113                 res.sort_value := public.naco_normalize(sort_text);
5114                 res.index_vector = to_tsvector('keyword'::regconfig, res.sort_value);
5115                 RETURN NEXT res;
5116             END IF;
5117
5118         END LOOP;
5119
5120     END LOOP;
5121
5122     RETURN;
5123 END;
5124 $func$ LANGUAGE PLPGSQL IMMUTABLE;
5125
5126 CREATE OR REPLACE FUNCTION url_verify.extract_urls ( session_id INT, item_id INT ) RETURNS INT AS $$
5127 DECLARE
5128     last_seen_tag TEXT;
5129     current_tag TEXT;
5130     current_sf TEXT;
5131     current_url TEXT;
5132     current_ord INT;
5133     current_url_pos INT;
5134     current_selector url_verify.url_selector%ROWTYPE;
5135 BEGIN
5136     current_ord := 1;
5137
5138     FOR current_selector IN SELECT * FROM url_verify.url_selector s WHERE s.session = session_id LOOP
5139         current_url_pos := 1;
5140         LOOP
5141             SELECT  (oils_xpath(current_selector.xpath || '/text()', b.marc))[current_url_pos] INTO current_url
5142               FROM  biblio.record_entry b
5143                     JOIN container.biblio_record_entry_bucket_item c ON (c.target_biblio_record_entry = b.id)
5144               WHERE c.id = item_id;
5145
5146             EXIT WHEN current_url IS NULL;
5147
5148             SELECT  (oils_xpath(current_selector.xpath || '/../@tag', b.marc))[current_url_pos] INTO current_tag
5149               FROM  biblio.record_entry b
5150                     JOIN container.biblio_record_entry_bucket_item c ON (c.target_biblio_record_entry = b.id)
5151               WHERE c.id = item_id;
5152
5153             IF current_tag IS NULL THEN
5154                 current_tag := last_seen_tag;
5155             ELSE
5156                 last_seen_tag := current_tag;
5157             END IF;
5158
5159             SELECT  (oils_xpath(current_selector.xpath || '/@code', b.marc))[current_url_pos] INTO current_sf
5160               FROM  biblio.record_entry b
5161                     JOIN container.biblio_record_entry_bucket_item c ON (c.target_biblio_record_entry = b.id)
5162               WHERE c.id = item_id;
5163
5164             INSERT INTO url_verify.url (session, item, url_selector, tag, subfield, ord, full_url)
5165               VALUES ( session_id, item_id, current_selector.id, current_tag, current_sf, current_ord, current_url);
5166
5167             current_url_pos := current_url_pos + 1;
5168             current_ord := current_ord + 1;
5169         END LOOP;
5170     END LOOP;
5171
5172     RETURN current_ord - 1;
5173 END;
5174 $$ LANGUAGE PLPGSQL;
5175
5176 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( rid BIGINT, default_joiner TEXT ) RETURNS SETOF metabib.field_entry_template AS $func$
5177 DECLARE
5178     bib     biblio.record_entry%ROWTYPE;
5179     idx     config.metabib_field%ROWTYPE;
5180     xfrm        config.xml_transform%ROWTYPE;
5181     prev_xfrm   TEXT;
5182     transformed_xml TEXT;
5183     xml_node    TEXT;
5184     xml_node_list   TEXT[];
5185     facet_text  TEXT;
5186     browse_text TEXT;
5187     sort_value  TEXT;
5188     raw_text    TEXT;
5189     curr_text   TEXT;
5190     joiner      TEXT := default_joiner; -- XXX will index defs supply a joiner?
5191     authority_text TEXT;
5192     authority_link BIGINT;
5193     output_row  metabib.field_entry_template%ROWTYPE;
5194 BEGIN
5195
5196     -- Start out with no field-use bools set
5197     output_row.browse_field = FALSE;
5198     output_row.facet_field = FALSE;
5199     output_row.search_field = FALSE;
5200
5201     -- Get the record
5202     SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
5203
5204     -- Loop over the indexing entries
5205     FOR idx IN SELECT * FROM config.metabib_field ORDER BY format LOOP
5206
5207         joiner := COALESCE(idx.joiner, default_joiner);
5208
5209         SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
5210
5211         -- See if we can skip the XSLT ... it's expensive
5212         IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
5213             -- Can't skip the transform
5214             IF xfrm.xslt <> '---' THEN
5215                 transformed_xml := oils_xslt_process(bib.marc,xfrm.xslt);
5216             ELSE
5217                 transformed_xml := bib.marc;
5218             END IF;
5219
5220             prev_xfrm := xfrm.name;
5221         END IF;
5222
5223         xml_node_list := oils_xpath( idx.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
5224
5225         raw_text := NULL;
5226         FOR xml_node IN SELECT x FROM unnest(xml_node_list) AS x LOOP
5227             CONTINUE WHEN xml_node !~ E'^\\s*<';
5228
5229             -- XXX much of this should be moved into oils_xpath_string...
5230             curr_text := ARRAY_TO_STRING(evergreen.array_remove_item_by_value(evergreen.array_remove_item_by_value(
5231                 oils_xpath( '//text()', -- get the content of all the nodes within the main selected node
5232                     REGEXP_REPLACE( xml_node, E'\\s+', ' ', 'g' ) -- Translate adjacent whitespace to a single space
5233                 ), ' '), ''),  -- throw away morally empty (bankrupt?) strings
5234                 joiner
5235             );
5236
5237             CONTINUE WHEN curr_text IS NULL OR curr_text = '';
5238
5239             IF raw_text IS NOT NULL THEN
5240                 raw_text := raw_text || joiner;
5241             END IF;
5242
5243             raw_text := COALESCE(raw_text,'') || curr_text;
5244
5245             -- autosuggest/metabib.browse_entry
5246             IF idx.browse_field THEN
5247
5248                 IF idx.browse_xpath IS NOT NULL AND idx.browse_xpath <> '' THEN
5249                     browse_text := oils_xpath_string( idx.browse_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
5250                 ELSE
5251                     browse_text := curr_text;
5252                 END IF;
5253
5254                 IF idx.browse_sort_xpath IS NOT NULL AND
5255                     idx.browse_sort_xpath <> '' THEN
5256
5257                     sort_value := oils_xpath_string(
5258                         idx.browse_sort_xpath, xml_node, joiner,
5259                         ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
5260                     );
5261                 ELSE
5262                     sort_value := browse_text;
5263                 END IF;
5264
5265                 output_row.field_class = idx.field_class;
5266                 output_row.field = idx.id;
5267                 output_row.source = rid;
5268                 output_row.value = BTRIM(REGEXP_REPLACE(browse_text, E'\\s+', ' ', 'g'));
5269                 output_row.sort_value :=
5270                     public.naco_normalize(sort_value);
5271
5272                 output_row.authority := NULL;
5273
5274                 IF idx.authority_xpath IS NOT NULL AND idx.authority_xpath <> '' THEN
5275                     authority_text := oils_xpath_string(
5276                         idx.authority_xpath, xml_node, joiner,
5277                         ARRAY[
5278                             ARRAY[xfrm.prefix, xfrm.namespace_uri],
5279                             ARRAY['xlink','http://www.w3.org/1999/xlink']
5280                         ]
5281                     );
5282
5283                     IF authority_text ~ '^\d+$' THEN
5284                         authority_link := authority_text::BIGINT;
5285                         PERFORM * FROM authority.record_entry WHERE id = authority_link;
5286                         IF FOUND THEN
5287                             output_row.authority := authority_link;
5288                         END IF;
5289                     END IF;
5290
5291                 END IF;
5292
5293                 output_row.browse_field = TRUE;
5294                 -- Returning browse rows with search_field = true for search+browse
5295                 -- configs allows us to retain granularity of being able to search
5296                 -- browse fields with "starts with" type operators (for example, for
5297                 -- titles of songs in music albums)
5298                 IF idx.search_field THEN
5299                     output_row.search_field = TRUE;
5300                 END IF;
5301                 RETURN NEXT output_row;
5302                 output_row.browse_field = FALSE;
5303                 output_row.search_field = FALSE;
5304                 output_row.sort_value := NULL;
5305             END IF;
5306
5307             -- insert raw node text for faceting
5308             IF idx.facet_field THEN
5309
5310                 IF idx.facet_xpath IS NOT NULL AND idx.facet_xpath <> '' THEN
5311                     facet_text := oils_xpath_string( idx.facet_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
5312                 ELSE
5313                     facet_text := curr_text;
5314                 END IF;
5315
5316                 output_row.field_class = idx.field_class;
5317                 output_row.field = -1 * idx.id;
5318                 output_row.source = rid;
5319                 output_row.value = BTRIM(REGEXP_REPLACE(facet_text, E'\\s+', ' ', 'g'));
5320
5321                 output_row.facet_field = TRUE;
5322                 RETURN NEXT output_row;
5323                 output_row.facet_field = FALSE;
5324             END IF;
5325
5326         END LOOP;
5327
5328         CONTINUE WHEN raw_text IS NULL OR raw_text = '';
5329
5330         -- insert combined node text for searching
5331         IF idx.search_field THEN
5332             output_row.field_class = idx.field_class;
5333             output_row.field = idx.id;
5334             output_row.source = rid;
5335             output_row.value = BTRIM(REGEXP_REPLACE(raw_text, E'\\s+', ' ', 'g'));
5336
5337             output_row.search_field = TRUE;
5338             RETURN NEXT output_row;
5339             output_row.search_field = FALSE;
5340         END IF;
5341
5342     END LOOP;
5343
5344 END;
5345
5346 $func$ LANGUAGE PLPGSQL;
5347
5348 CREATE OR REPLACE FUNCTION metabib.reingest_record_attributes (rid BIGINT, pattr_list TEXT[] DEFAULT NULL, prmarc TEXT DEFAULT NULL, rdeleted BOOL DEFAULT TRUE) RETURNS VOID AS $func$
5349 DECLARE
5350     transformed_xml TEXT;
5351     rmarc           TEXT := prmarc;
5352     tmp_val         TEXT;
5353     prev_xfrm       TEXT;
5354     normalizer      RECORD;
5355     xfrm            config.xml_transform%ROWTYPE;
5356     attr_vector     INT[] := '{}'::INT[];
5357     attr_vector_tmp INT[];
5358     attr_list       TEXT[] := pattr_list;
5359     attr_value      TEXT[];
5360     norm_attr_value TEXT[];
5361     tmp_xml         TEXT;
5362     attr_def        config.record_attr_definition%ROWTYPE;
5363     ccvm_row        config.coded_value_map%ROWTYPE;
5364 BEGIN
5365
5366     IF attr_list IS NULL OR rdeleted THEN -- need to do the full dance on INSERT or undelete
5367         SELECT ARRAY_AGG(name) INTO attr_list FROM config.record_attr_definition;
5368     END IF;
5369
5370     IF rmarc IS NULL THEN
5371         SELECT marc INTO rmarc FROM biblio.record_entry WHERE id = rid;
5372     END IF;
5373
5374     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE NOT composite AND name = ANY( attr_list ) ORDER BY format LOOP
5375
5376         attr_value := '{}'::TEXT[];
5377         norm_attr_value := '{}'::TEXT[];
5378         attr_vector_tmp := '{}'::INT[];
5379
5380         SELECT * INTO ccvm_row FROM config.coded_value_map c WHERE c.ctype = attr_def.name LIMIT 1; 
5381
5382         -- tag+sf attrs only support SVF
5383         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
5384             SELECT  ARRAY[ARRAY_TO_STRING(ARRAY_AGG(value), COALESCE(attr_def.joiner,' '))] INTO attr_value
5385               FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
5386               WHERE record = rid
5387                     AND tag LIKE attr_def.tag
5388                     AND CASE
5389                         WHEN attr_def.sf_list IS NOT NULL 
5390                             THEN POSITION(subfield IN attr_def.sf_list) > 0
5391                         ELSE TRUE
5392                     END
5393               GROUP BY tag
5394               ORDER BY tag
5395               LIMIT 1;
5396
5397         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
5398             attr_value := vandelay.marc21_extract_fixed_field_list(rmarc, attr_def.fixed_field);
5399
5400             IF NOT attr_def.multi THEN
5401                 attr_value := ARRAY[attr_value[1]];
5402             END IF;
5403
5404         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
5405
5406             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
5407         
5408             -- See if we can skip the XSLT ... it's expensive
5409             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
5410                 -- Can't skip the transform
5411                 IF xfrm.xslt <> '---' THEN
5412                     transformed_xml := oils_xslt_process(rmarc,xfrm.xslt);
5413                 ELSE
5414                     transformed_xml := rmarc;
5415                 END IF;
5416     
5417                 prev_xfrm := xfrm.name;
5418             END IF;
5419
5420             IF xfrm.name IS NULL THEN
5421                 -- just grab the marcxml (empty) transform
5422                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
5423                 prev_xfrm := xfrm.name;
5424             END IF;
5425
5426             FOR tmp_xml IN SELECT oils_xpath(attr_def.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]) LOOP
5427                 tmp_val := oils_xpath_string(
5428                                 '//*',
5429                                 tmp_xml,
5430                                 COALESCE(attr_def.joiner,' '),
5431                                 ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
5432                             );
5433                 IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
5434                     attr_value := attr_value || tmp_val;
5435                     EXIT WHEN NOT attr_def.multi;
5436                 END IF;
5437             END LOOP;
5438
5439         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
5440             SELECT  ARRAY_AGG(m.value) INTO attr_value
5441               FROM  vandelay.marc21_physical_characteristics(rmarc) v
5442                     LEFT JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
5443               WHERE v.subfield = attr_def.phys_char_sf AND (m.value IS NOT NULL AND BTRIM(m.value) <> '')
5444                     AND ( ccvm_row.id IS NULL OR ( ccvm_row.id IS NOT NULL AND v.id IS NOT NULL) );
5445
5446             IF NOT attr_def.multi THEN
5447                 attr_value := ARRAY[attr_value[1]];
5448             END IF;
5449
5450         END IF;
5451
5452                 -- apply index normalizers to attr_value
5453         FOR tmp_val IN SELECT value FROM UNNEST(attr_value) x(value) LOOP
5454             FOR normalizer IN
5455                 SELECT  n.func AS func,
5456                         n.param_count AS param_count,
5457                         m.params AS params
5458                   FROM  config.index_normalizer n
5459                         JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
5460                   WHERE attr = attr_def.name
5461                   ORDER BY m.pos LOOP
5462                     EXECUTE 'SELECT ' || normalizer.func || '(' ||
5463                     COALESCE( quote_literal( tmp_val ), 'NULL' ) ||
5464                         CASE
5465                             WHEN normalizer.param_count > 0
5466                                 THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
5467                                 ELSE ''
5468                             END ||
5469                     ')' INTO tmp_val;
5470
5471             END LOOP;
5472             IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
5473                 norm_attr_value := norm_attr_value || tmp_val;
5474             END IF;
5475         END LOOP;
5476         
5477         IF attr_def.filter THEN
5478             -- Create unknown uncontrolled values and find the IDs of the values
5479             IF ccvm_row.id IS NULL THEN
5480                 FOR tmp_val IN SELECT value FROM UNNEST(norm_attr_value) x(value) LOOP
5481                     IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
5482                         BEGIN -- use subtransaction to isolate unique constraint violations
5483                             INSERT INTO metabib.uncontrolled_record_attr_value ( attr, value ) VALUES ( attr_def.name, tmp_val );
5484                         EXCEPTION WHEN unique_violation THEN END;
5485                     END IF;
5486                 END LOOP;
5487
5488                 SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.uncontrolled_record_attr_value WHERE attr = attr_def.name AND value = ANY( norm_attr_value );
5489             ELSE
5490                 SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM config.coded_value_map WHERE ctype = attr_def.name AND code = ANY( norm_attr_value );
5491             END IF;
5492
5493             -- Add the new value to the vector
5494             attr_vector := attr_vector || attr_vector_tmp;
5495         END IF;
5496
5497         IF attr_def.sorter AND norm_attr_value[1] IS NOT NULL THEN
5498             DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
5499             INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, norm_attr_value[1]);
5500         END IF;
5501
5502     END LOOP;
5503
5504 /* We may need to rewrite the vlist to contain
5505    the intersection of new values for requested
5506    attrs and old values for ignored attrs. To
5507    do this, we take the old attr vlist and
5508    subtract any values that are valid for the
5509    requested attrs, and then add back the new
5510    set of attr values. */
5511
5512     IF ARRAY_LENGTH(pattr_list, 1) > 0 THEN 
5513         SELECT vlist INTO attr_vector_tmp FROM metabib.record_attr_vector_list WHERE source = rid;
5514         SELECT attr_vector_tmp - ARRAY_AGG(id::INT) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
5515         attr_vector := attr_vector || attr_vector_tmp;
5516     END IF;
5517
5518     -- On to composite attributes, now that the record attrs have been pulled.  Processed in name order, so later composite
5519     -- attributes can depend on earlier ones.
5520     PERFORM metabib.compile_composite_attr_cache_init();
5521     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE composite AND name = ANY( attr_list ) ORDER BY name LOOP
5522
5523         FOR ccvm_row IN SELECT * FROM config.coded_value_map c WHERE c.ctype = attr_def.name ORDER BY value LOOP
5524
5525             tmp_val := metabib.compile_composite_attr( ccvm_row.id );
5526             CONTINUE WHEN tmp_val IS NULL OR tmp_val = ''; -- nothing to do
5527
5528             IF attr_def.filter THEN
5529                 IF attr_vector @@ tmp_val::query_int THEN
5530                     attr_vector = attr_vector + intset(ccvm_row.id);
5531                     EXIT WHEN NOT attr_def.multi;
5532                 END IF;
5533             END IF;
5534
5535             IF attr_def.sorter THEN
5536                 IF attr_vector @@ tmp_val THEN
5537                     DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
5538                     INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, ccvm_row.code);
5539                 END IF;
5540             END IF;
5541
5542         END LOOP;
5543
5544     END LOOP;
5545
5546     IF ARRAY_LENGTH(attr_vector, 1) > 0 THEN
5547         IF rdeleted THEN -- initial insert OR revivication
5548             DELETE FROM metabib.record_attr_vector_list WHERE source = rid;
5549             INSERT INTO metabib.record_attr_vector_list (source, vlist) VALUES (rid, attr_vector);
5550         ELSE
5551             UPDATE metabib.record_attr_vector_list SET vlist = attr_vector WHERE source = rid;
5552         END IF;
5553     END IF;
5554
5555 END;
5556
5557 $func$ LANGUAGE PLPGSQL;
5558
5559
5560 ALTER TABLE authority.record_entry DISABLE TRIGGER a_marcxml_is_well_formed;
5561 ALTER TABLE authority.record_entry DISABLE TRIGGER aaa_auth_ingest_or_delete;
5562 ALTER TABLE authority.record_entry DISABLE TRIGGER b_maintain_901;
5563 ALTER TABLE authority.record_entry DISABLE TRIGGER c_maintain_control_numbers;
5564 ALTER TABLE authority.record_entry DISABLE TRIGGER map_thesaurus_to_control_set;
5565
5566
5567 SELECT evergreen.upgrade_deps_block_check('0875', :eg_version);
5568
5569 ALTER TABLE authority.record_entry ADD COLUMN heading TEXT, ADD COLUMN simple_heading TEXT;
5570
5571 DROP INDEX IF EXISTS authority.unique_by_heading_and_thesaurus;
5572 DROP INDEX IF EXISTS authority.by_heading_and_thesaurus;
5573 DROP INDEX IF EXISTS authority.by_heading;
5574
5575 -- Update without indexes for HOT update
5576 UPDATE  authority.record_entry
5577   SET   heading = authority.normalize_heading( marc ),
5578         simple_heading = authority.simple_normalize_heading( marc );
5579
5580 CREATE INDEX by_heading_and_thesaurus ON authority.record_entry (heading) WHERE deleted IS FALSE or deleted = FALSE;
5581 CREATE INDEX by_heading ON authority.record_entry (simple_heading) WHERE deleted IS FALSE or deleted = FALSE;
5582
5583 -- Add the trigger
5584 CREATE OR REPLACE FUNCTION authority.normalize_heading_for_upsert () RETURNS TRIGGER AS $f$
5585 BEGIN
5586     NEW.heading := authority.normalize_heading( NEW.marc );
5587     NEW.simple_heading := authority.simple_normalize_heading( NEW.marc );
5588     RETURN NEW;
5589 END;
5590 $f$ LANGUAGE PLPGSQL;
5591
5592 CREATE TRIGGER update_headings_tgr BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE authority.normalize_heading_for_upsert();
5593
5594 ALTER FUNCTION authority.normalize_heading(TEXT, BOOL) STABLE STRICT;
5595 ALTER FUNCTION authority.normalize_heading(TEXT) STABLE STRICT;
5596 ALTER FUNCTION authority.simple_normalize_heading(TEXT) STABLE STRICT;
5597 ALTER FUNCTION authority.simple_heading_set(TEXT) STABLE STRICT;
5598
5599
5600 ALTER TABLE authority.record_entry ENABLE TRIGGER a_marcxml_is_well_formed;
5601 ALTER TABLE authority.record_entry ENABLE TRIGGER aaa_auth_ingest_or_delete;
5602 ALTER TABLE authority.record_entry ENABLE TRIGGER b_maintain_901;
5603 ALTER TABLE authority.record_entry ENABLE TRIGGER c_maintain_control_numbers;
5604 ALTER TABLE authority.record_entry ENABLE TRIGGER map_thesaurus_to_control_set;
5605
5606
5607
5608 SELECT evergreen.upgrade_deps_block_check('0876', :eg_version);
5609
5610 INSERT INTO permission.perm_list ( code, description ) VALUES
5611  ( 'group_application.user.staff.admin.system_admin', oils_i18n_gettext( '',
5612     'Allow a user to add/remove users to/from the "System Administrator" group', 'ppl', 'description' )),
5613  ( 'group_application.user.staff.cat_admin', oils_i18n_gettext( '', 
5614     'Allow a user to add/remove users to/from the "Cataloging Administrator" group', 'ppl', 'description' )),
5615  ( 'group_application.user.staff.circ_admin', oils_i18n_gettext( '', 
5616     'Allow a user to add/remove users to/from the "Circulation Administrator" group', 'ppl', 'description' )),
5617  ( 'group_application.user.staff.data_review', oils_i18n_gettext( '', 
5618     'Allow a user to add/remove users to/from the "Data Review" group', 'ppl', 'description' )),
5619  ( 'group_application.user.staff.volunteers', oils_i18n_gettext( '', 
5620     'Allow a user to add/remove users to/from the "Volunteers" group', 'ppl', 'description' ))
5621 ;
5622
5623
5624 SELECT evergreen.upgrade_deps_block_check('0877', :eg_version);
5625
5626 -- Don't use Series search field as the browse field
5627 UPDATE config.metabib_field SET
5628         browse_field = FALSE,
5629         browse_xpath = NULL,
5630         browse_sort_xpath = NULL,
5631         xpath = $$//mods32:mods/mods32:relatedItem[@type="series"]/mods32:titleInfo[not(@type="nfi")]$$
5632 WHERE id = 1;
5633
5634 -- Create a new series browse config
5635 INSERT INTO config.metabib_field ( id, field_class, name, label, format, xpath, search_field, authority_xpath, browse_field, browse_sort_xpath ) VALUES
5636     (32, 'series', 'browse', oils_i18n_gettext(32, 'Series Title (Browse)', 'cmf', 'label'), 'mods32', $$//mods32:mods/mods32:relatedItem[@type="series"]/mods32:titleInfo[@type="nfi"]$$, FALSE, '//@xlink:href', TRUE, $$*[local-name() != "nonSort"]$$ );
5637
5638 SELECT evergreen.upgrade_deps_block_check('0878', :eg_version);
5639
5640 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_field_entries( bib_id BIGINT, skip_facet BOOL DEFAULT FALSE, skip_browse BOOL DEFAULT FALSE, skip_search BOOL DEFAULT FALSE ) RETURNS VOID AS $func$
5641 DECLARE
5642     fclass          RECORD;
5643     ind_data        metabib.field_entry_template%ROWTYPE;
5644     mbe_row         metabib.browse_entry%ROWTYPE;
5645     mbe_id          BIGINT;
5646     b_skip_facet    BOOL;
5647     b_skip_browse   BOOL;
5648     b_skip_search   BOOL;
5649     value_prepped   TEXT;
5650 BEGIN
5651
5652     SELECT COALESCE(NULLIF(skip_facet, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_facet_indexing' AND enabled)) INTO b_skip_facet;
5653     SELECT COALESCE(NULLIF(skip_browse, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_browse_indexing' AND enabled)) INTO b_skip_browse;
5654     SELECT COALESCE(NULLIF(skip_search, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_search_indexing' AND enabled)) INTO b_skip_search;
5655
5656     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
5657     IF NOT FOUND THEN
5658         IF NOT b_skip_search THEN
5659             FOR fclass IN SELECT * FROM config.metabib_class LOOP
5660                 -- RAISE NOTICE 'Emptying out %', fclass.name;
5661                 EXECUTE $$DELETE FROM metabib.$$ || fclass.name || $$_field_entry WHERE source = $$ || bib_id;
5662             END LOOP;
5663         END IF;
5664         IF NOT b_skip_facet THEN
5665             DELETE FROM metabib.facet_entry WHERE source = bib_id;
5666         END IF;
5667         IF NOT b_skip_browse THEN
5668             DELETE FROM metabib.browse_entry_def_map WHERE source = bib_id;
5669         END IF;
5670     END IF;
5671
5672     FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( bib_id ) LOOP
5673
5674         -- don't store what has been normalized away
5675         CONTINUE WHEN ind_data.value IS NULL;
5676
5677         IF ind_data.field < 0 THEN
5678             ind_data.field = -1 * ind_data.field;
5679         END IF;
5680
5681         IF ind_data.facet_field AND NOT b_skip_facet THEN
5682             INSERT INTO metabib.facet_entry (field, source, value)
5683                 VALUES (ind_data.field, ind_data.source, ind_data.value);
5684         END IF;
5685
5686         IF ind_data.browse_field AND NOT b_skip_browse THEN
5687             -- A caveat about this SELECT: this should take care of replacing
5688             -- old mbe rows when data changes, but not if normalization (by
5689             -- which I mean specifically the output of
5690             -- evergreen.oils_tsearch2()) changes.  It may or may not be
5691             -- expensive to add a comparison of index_vector to index_vector
5692             -- to the WHERE clause below.
5693
5694             CONTINUE WHEN ind_data.sort_value IS NULL;
5695
5696             value_prepped := metabib.browse_normalize(ind_data.value, ind_data.field);
5697             SELECT INTO mbe_row * FROM metabib.browse_entry
5698                 WHERE value = value_prepped AND sort_value = ind_data.sort_value;
5699
5700             IF FOUND THEN
5701                 mbe_id := mbe_row.id;
5702             ELSE
5703                 INSERT INTO metabib.browse_entry
5704                     ( value, sort_value ) VALUES
5705                     ( value_prepped, ind_data.sort_value );
5706
5707                 mbe_id := CURRVAL('metabib.browse_entry_id_seq'::REGCLASS);
5708             END IF;
5709
5710             INSERT INTO metabib.browse_entry_def_map (entry, def, source, authority)
5711                 VALUES (mbe_id, ind_data.field, ind_data.source, ind_data.authority);
5712         END IF;
5713
5714         IF ind_data.search_field AND NOT b_skip_search THEN
5715             -- Avoid inserting duplicate rows
5716             EXECUTE 'SELECT 1 FROM metabib.' || ind_data.field_class ||
5717                 '_field_entry WHERE field = $1 AND source = $2 AND value = $3'
5718                 INTO mbe_id USING ind_data.field, ind_data.source, ind_data.value;
5719                 -- RAISE NOTICE 'Search for an already matching row returned %', mbe_id;
5720             IF mbe_id IS NULL THEN
5721                 EXECUTE $$
5722                 INSERT INTO metabib.$$ || ind_data.field_class || $$_field_entry (field, source, value)
5723                     VALUES ($$ ||
5724                         quote_literal(ind_data.field) || $$, $$ ||
5725                         quote_literal(ind_data.source) || $$, $$ ||
5726                         quote_literal(ind_data.value) ||
5727                     $$);$$;
5728             END IF;
5729         END IF;
5730
5731     END LOOP;
5732
5733     IF NOT b_skip_search THEN
5734         PERFORM metabib.update_combined_index_vectors(bib_id);
5735     END IF;
5736
5737     RETURN;
5738 END;
5739 $func$ LANGUAGE PLPGSQL;
5740
5741
5742
5743 SELECT evergreen.upgrade_deps_block_check('0879', :eg_version);
5744
5745 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_jrow(
5746     node vandelay.match_set_point,
5747     tags_rstore HSTORE
5748 ) RETURNS VOID AS $$
5749 DECLARE
5750     jrow        TEXT;
5751     my_alias    TEXT;
5752     op          TEXT;
5753     tagkey      TEXT;
5754     caseless    BOOL;
5755     jrow_count  INT;
5756     my_using    TEXT;
5757     my_join     TEXT;
5758 BEGIN
5759     -- remember $1 is tags_rstore, and $2 is svf_rstore
5760
5761     caseless := FALSE;
5762     SELECT COUNT(*) INTO jrow_count FROM _vandelay_tmp_jrows;
5763     IF jrow_count > 0 THEN
5764         my_using := ' USING (record)';
5765         my_join := 'FULL OUTER JOIN';
5766     ELSE
5767         my_using := '';
5768         my_join := 'FROM';
5769     END IF;
5770
5771     IF node.tag IS NOT NULL THEN
5772         caseless := (node.tag IN ('020', '022', '024'));
5773         tagkey := node.tag;
5774         IF node.subfield IS NOT NULL THEN
5775             tagkey := tagkey || node.subfield;
5776         END IF;
5777     END IF;
5778
5779     IF node.negate THEN
5780         IF caseless THEN
5781             op := 'NOT LIKE';
5782         ELSE
5783             op := '<>';
5784         END IF;
5785     ELSE
5786         IF caseless THEN
5787             op := 'LIKE';
5788         ELSE
5789             op := '=';
5790         END IF;
5791     END IF;
5792
5793     my_alias := 'n' || node.id::TEXT;
5794
5795     jrow := my_join || ' (SELECT *, ';
5796     IF node.tag IS NOT NULL THEN
5797         jrow := jrow  || node.quality ||
5798             ' AS quality FROM metabib.full_rec mfr WHERE mfr.tag = ''' ||
5799             node.tag || '''';
5800         IF node.subfield IS NOT NULL THEN
5801             jrow := jrow || ' AND mfr.subfield = ''' ||
5802                 node.subfield || '''';
5803         END IF;
5804         jrow := jrow || ' AND (';
5805         jrow := jrow || vandelay._node_tag_comparisons(caseless, op, tags_rstore, tagkey);
5806         jrow := jrow || ')) ' || my_alias || my_using || E'\n';
5807     ELSE    -- svf
5808         jrow := jrow || 'id AS record, ' || node.quality ||
5809             ' AS quality FROM metabib.record_attr_flat mraf WHERE mraf.attr = ''' ||
5810             node.svf || ''' AND mraf.value ' || op || ' $2->''' || node.svf || ''') ' ||
5811             my_alias || my_using || E'\n';
5812     END IF;
5813     INSERT INTO _vandelay_tmp_jrows (j) VALUES (jrow);
5814 END;
5815 $$ LANGUAGE PLPGSQL;
5816
5817
5818 COMMIT;
5819
5820 -- Not running changes from example.reporter-extension.sql since these are
5821 -- not installed by default, but including a helpful note.
5822 \qecho
5823 \qecho **** NOTICE ****
5824 \qecho 'There were changes in example.reporter-extension.sql.'
5825 \qecho 'Please run that script again if you use it in your system'
5826 \qecho 'to apply new changes.'
5827 \qecho
5828 \qecho
5829 \qecho **** Certain improvements in 2.6, particularly attribute improvements,
5830 \qecho **** require a reingest of all your bib records.  In order to allow
5831 \qecho **** this to continue without locking your entire bibliographic data
5832 \qecho **** set, consider generating an SQL script with the following queries,
5833 \qecho **** then running it via psql.
5834 \qecho ****
5835 \qecho **** If you have a large number of bibs (100,000+), please consider this
5836 \qecho **** as a starting point only, as you will likely wish to parallelize
5837 \qecho **** this is some fashion.
5838 \qecho ****
5839 \qecho **** If you require a more responsive catalog/database while reingesting,
5840 \qecho **** consider adding 'pg_sleep()' calls between each reingest update.
5841 \qecho
5842 \qecho '\\t'
5843 \qecho '\\o /tmp/reingest_2.6_bib_recs.sql'
5844 \qecho 'SELECT ''-- Grab current setting'';'
5845 \qecho 'SELECT ''\\set force_reingest '' || enabled FROM config.internal_flag WHERE name = ''ingest.reingest.force_on_same_marc'';'
5846 \qecho 'SELECT ''update config.internal_flag set enabled = true where name = ''''ingest.reingest.force_on_same_marc'''';'';'
5847 \qecho 'SELECT ''update biblio.record_entry set id = id where id = '' || id || '';'' FROM biblio.record_entry WHERE NOT DELETED AND id > 0;'
5848 \qecho 'SELECT ''-- Restore previous setting'';'
5849 \qecho 'SELECT ''update config.internal_flag set enabled = :force_reingest where name = \'\'ingest.reingest.force_on_same_marc\'\';'';'
5850 \qecho '\\o'
5851 \qecho '\\t'