Forward-port 2.6.0 version upgrade script
[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
4 \qecho
5 \qecho **** NOTICE ****
6 \qecho 'We are disabling all triggers for authority.record_entry outside the '
7 \qecho 'transaction.  If this upgrade fails, you may want to double-check that '
8 \qecho 'triggers are reactivated, e.g.:'
9 \qecho 'ALTER TABLE authority.record_entry ENABLE TRIGGER ALL;'
10 \qecho
11 ALTER TABLE authority.record_entry DISABLE TRIGGER a_marcxml_is_well_formed;
12 ALTER TABLE authority.record_entry DISABLE TRIGGER aaa_auth_ingest_or_delete;
13 ALTER TABLE authority.record_entry DISABLE TRIGGER b_maintain_901;
14 ALTER TABLE authority.record_entry DISABLE TRIGGER c_maintain_control_numbers;
15 ALTER TABLE authority.record_entry DISABLE TRIGGER map_thesaurus_to_control_set;
16
17 BEGIN;
18 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('2.6.0', :eg_version);
19
20 -- check whether patch can be applied
21 SELECT evergreen.upgrade_deps_block_check('0851', :eg_version);
22
23 CREATE OR REPLACE FUNCTION evergreen.maintain_901 () RETURNS TRIGGER AS $func$
24 use strict;
25 use MARC::Record;
26 use MARC::File::XML (BinaryEncoding => 'UTF-8');
27 use MARC::Charset;
28 use Encode;
29 use Unicode::Normalize;
30
31 MARC::Charset->assume_unicode(1);
32
33 my $schema = $_TD->{table_schema};
34 my $marc = MARC::Record->new_from_xml($_TD->{new}{marc});
35
36 my @old901s = $marc->field('901');
37 $marc->delete_fields(@old901s);
38
39 if ($schema eq 'biblio') {
40     my $tcn_value = $_TD->{new}{tcn_value};
41
42     # Set TCN value to record ID?
43     my $id_as_tcn = spi_exec_query("
44         SELECT enabled
45         FROM config.global_flag
46         WHERE name = 'cat.bib.use_id_for_tcn'
47     ");
48     if (($id_as_tcn->{processed}) && $id_as_tcn->{rows}[0]->{enabled} eq 't') {
49         $tcn_value = $_TD->{new}{id}; 
50         $_TD->{new}{tcn_value} = $tcn_value;
51     }
52
53     my $new_901 = MARC::Field->new("901", " ", " ",
54         "a" => $tcn_value,
55         "b" => $_TD->{new}{tcn_source},
56         "c" => $_TD->{new}{id},
57         "t" => $schema
58     );
59
60     if ($_TD->{new}{owner}) {
61         $new_901->add_subfields("o" => $_TD->{new}{owner});
62     }
63
64     if ($_TD->{new}{share_depth}) {
65         $new_901->add_subfields("d" => $_TD->{new}{share_depth});
66     }
67
68     $marc->append_fields($new_901);
69 } elsif ($schema eq 'authority') {
70     my $new_901 = MARC::Field->new("901", " ", " ",
71         "c" => $_TD->{new}{id},
72         "t" => $schema,
73     );
74     $marc->append_fields($new_901);
75 } elsif ($schema eq 'serial') {
76     my $new_901 = MARC::Field->new("901", " ", " ",
77         "c" => $_TD->{new}{id},
78         "t" => $schema,
79         "o" => $_TD->{new}{owning_lib},
80     );
81
82     if ($_TD->{new}{record}) {
83         $new_901->add_subfields("r" => $_TD->{new}{record});
84     }
85
86     $marc->append_fields($new_901);
87 } else {
88     my $new_901 = MARC::Field->new("901", " ", " ",
89         "c" => $_TD->{new}{id},
90         "t" => $schema,
91     );
92     $marc->append_fields($new_901);
93 }
94
95 my $xml = $marc->as_xml_record();
96 $xml =~ s/\n//sgo;
97 $xml =~ s/^<\?xml.+\?\s*>//go;
98 $xml =~ s/>\s+</></go;
99 $xml =~ s/\p{Cc}//go;
100
101 # Embed a version of OpenILS::Application::AppUtils->entityize()
102 # to avoid having to set PERL5LIB for PostgreSQL as well
103
104 $xml = NFC($xml);
105
106 # Convert raw ampersands to entities
107 $xml =~ s/&(?!\S+;)/&amp;/gso;
108
109 # Convert Unicode characters to entities
110 $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
111
112 $xml =~ s/[\x00-\x1f]//go;
113 $_TD->{new}{marc} = $xml;
114
115 return "MODIFY";
116 $func$ LANGUAGE PLPERLU;
117
118 CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
119 use strict;
120 use MARC::Record;
121 use MARC::File::XML (BinaryEncoding => 'UTF-8');
122 use MARC::Charset;
123 use Encode;
124 use Unicode::Normalize;
125
126 MARC::Charset->assume_unicode(1);
127
128 my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
129 my $schema = $_TD->{table_schema};
130 my $rec_id = $_TD->{new}{id};
131
132 # Short-circuit if maintaining control numbers per MARC21 spec is not enabled
133 my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
134 if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
135     return;
136 }
137
138 # Get the control number identifier from an OU setting based on $_TD->{new}{owner}
139 my $ou_cni = 'EVRGRN';
140
141 my $owner;
142 if ($schema eq 'serial') {
143     $owner = $_TD->{new}{owning_lib};
144 } else {
145     # are.owner and bre.owner can be null, so fall back to the consortial setting
146     $owner = $_TD->{new}{owner} || 1;
147 }
148
149 my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
150 if ($ous_rv->{processed}) {
151     $ou_cni = $ous_rv->{rows}[0]->{value};
152     $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
153 } else {
154     # Fall back to the shortname of the OU if there was no OU setting
155     $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
156     if ($ous_rv->{processed}) {
157         $ou_cni = $ous_rv->{rows}[0]->{shortname};
158     }
159 }
160
161 my ($create, $munge) = (0, 0);
162
163 my @scns = $record->field('035');
164
165 foreach my $id_field ('001', '003') {
166     my $spec_value;
167     my @controls = $record->field($id_field);
168
169     if ($id_field eq '001') {
170         $spec_value = $rec_id;
171     } else {
172         $spec_value = $ou_cni;
173     }
174
175     # Create the 001/003 if none exist
176     if (scalar(@controls) == 1) {
177         # Only one field; check to see if we need to munge it
178         unless (grep $_->data() eq $spec_value, @controls) {
179             $munge = 1;
180         }
181     } else {
182         # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
183         foreach my $control (@controls) {
184             $record->delete_field($control);
185         }
186         $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
187         $create = 1;
188     }
189 }
190
191 my $cn = $record->field('001')->data();
192 # Special handling of OCLC numbers, often found in records that lack 003
193 if ($cn =~ /^o(c[nm]|n)\d/) {
194     $cn =~ s/^o(c[nm]|n)0*(\d+)/$2/;
195     $record->field('003')->data('OCoLC');
196     $create = 0;
197 }
198
199 # Now, if we need to munge the 001, we will first push the existing 001/003
200 # into the 035; but if the record did not have one (and one only) 001 and 003
201 # to begin with, skip this process
202 if ($munge and not $create) {
203
204     my $scn = "(" . $record->field('003')->data() . ")" . $cn;
205
206     # Do not create duplicate 035 fields
207     unless (grep $_->subfield('a') eq $scn, @scns) {
208         $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
209     }
210 }
211
212 # Set the 001/003 and update the MARC
213 if ($create or $munge) {
214     $record->field('001')->data($rec_id);
215     $record->field('003')->data($ou_cni);
216
217     my $xml = $record->as_xml_record();
218     $xml =~ s/\n//sgo;
219     $xml =~ s/^<\?xml.+\?\s*>//go;
220     $xml =~ s/>\s+</></go;
221     $xml =~ s/\p{Cc}//go;
222
223     # Embed a version of OpenILS::Application::AppUtils->entityize()
224     # to avoid having to set PERL5LIB for PostgreSQL as well
225
226     $xml = NFC($xml);
227
228     # Convert raw ampersands to entities
229     $xml =~ s/&(?!\S+;)/&amp;/gso;
230
231     # Convert Unicode characters to entities
232     $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
233
234     $xml =~ s/[\x00-\x1f]//go;
235     $_TD->{new}{marc} = $xml;
236
237     return "MODIFY";
238 }
239
240 return;
241 $func$ LANGUAGE PLPERLU;
242
243 CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
244
245     use strict;
246     use Unicode::Normalize;
247     use Encode;
248
249     my $str = shift;
250     my $sf = shift;
251
252     # Apply NACO normalization to input string; based on
253     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
254     #
255     # Note that unlike a strict reading of the NACO normalization rules,
256     # output is returned as lowercase instead of uppercase for compatibility
257     # with previous versions of the Evergreen naco_normalize routine.
258
259     # Convert to upper-case first; even though final output will be lowercase, doing this will
260     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
261     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
262     $str = uc $str;
263
264     # remove non-filing strings
265     $str =~ s/\x{0098}.*?\x{009C}//g;
266
267     $str = NFKD($str);
268
269     # additional substitutions - 3.6.
270     $str =~ s/\x{00C6}/AE/g;
271     $str =~ s/\x{00DE}/TH/g;
272     $str =~ s/\x{0152}/OE/g;
273     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
274
275     # transformations based on Unicode category codes
276     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
277
278         if ($sf && $sf =~ /^a/o) {
279                 my $commapos = index($str, ',');
280                 if ($commapos > -1) {
281                         if ($commapos != length($str) - 1) {
282                 $str =~ s/,/\x07/; # preserve first comma
283                         }
284                 }
285         }
286
287     # since we've stripped out the control characters, we can now
288     # use a few as placeholders temporarily
289     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
290     $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;
291     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
292
293     # decimal digits
294     $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/;
295
296     # intentionally skipping step 8 of the NACO algorithm; if the string
297     # gets normalized away, that's fine.
298
299     # leading and trailing spaces
300     $str =~ s/\s+/ /g;
301     $str =~ s/^\s+//;
302     $str =~ s/\s+$//g;
303
304     return lc $str;
305 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
306
307 -- Currently, the only difference from naco_normalize is that search_normalize
308 -- turns apostrophes into spaces, while naco_normalize collapses them.
309 CREATE OR REPLACE FUNCTION public.search_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
310
311     use strict;
312     use Unicode::Normalize;
313     use Encode;
314
315     my $str = shift;
316     my $sf = shift;
317
318     # Apply NACO normalization to input string; based on
319     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
320     #
321     # Note that unlike a strict reading of the NACO normalization rules,
322     # output is returned as lowercase instead of uppercase for compatibility
323     # with previous versions of the Evergreen naco_normalize routine.
324
325     # Convert to upper-case first; even though final output will be lowercase, doing this will
326     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
327     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
328     $str = uc $str;
329
330     # remove non-filing strings
331     $str =~ s/\x{0098}.*?\x{009C}//g;
332
333     $str = NFKD($str);
334
335     # additional substitutions - 3.6.
336     $str =~ s/\x{00C6}/AE/g;
337     $str =~ s/\x{00DE}/TH/g;
338     $str =~ s/\x{0152}/OE/g;
339     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}][/DDOLl/d;
340
341     # transformations based on Unicode category codes
342     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
343
344         if ($sf && $sf =~ /^a/o) {
345                 my $commapos = index($str, ',');
346                 if ($commapos > -1) {
347                         if ($commapos != length($str) - 1) {
348                 $str =~ s/,/\x07/; # preserve first comma
349                         }
350                 }
351         }
352
353     # since we've stripped out the control characters, we can now
354     # use a few as placeholders temporarily
355     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
356     $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;
357     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
358
359     # decimal digits
360     $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/;
361
362     # intentionally skipping step 8 of the NACO algorithm; if the string
363     # gets normalized away, that's fine.
364
365     # leading and trailing spaces
366     $str =~ s/\s+/ /g;
367     $str =~ s/^\s+//;
368     $str =~ s/\s+$//g;
369
370     return lc $str;
371 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
372
373 -- Evergreen DB patch XXXX.data.prefer_external_url_OUS.sql
374 --
375 -- FIXME: insert description of change, if needed
376 --
377
378 -- check whether patch can be applied
379 SELECT evergreen.upgrade_deps_block_check('0853', :eg_version);
380
381 INSERT into config.org_unit_setting_type
382 ( name, grp, label, description, datatype, fm_class ) VALUES
383 ( 'lib.prefer_external_url', 'lib',
384   'Use external "library information URL" in copy table, if available',
385   '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.',
386   'bool', null
387 );
388
389
390 SELECT evergreen.upgrade_deps_block_check('0854', :eg_version);
391
392 INSERT INTO permission.perm_list ( id, code, description ) VALUES (
393     553,
394     'UPDATE_ORG_UNIT_SETTING.circ.min_item_price',
395     oils_i18n_gettext(
396         553,
397         'UPDATE_ORG_UNIT_SETTING.circ.min_item_price',
398         'ppl',
399         'description'
400     )
401 ), (
402         554,
403     'UPDATE_ORG_UNIT_SETTING.circ.max_item_price',
404     oils_i18n_gettext(
405         554,
406         'UPDATE_ORG_UNIT_SETTING.circ.max_item_price',
407         'ppl',
408         'description'
409     )
410 );
411
412 INSERT into config.org_unit_setting_type
413     ( name, grp, label, description, datatype, fm_class )
414 VALUES (
415     'circ.min_item_price',
416         'finance',
417     oils_i18n_gettext(
418         'circ.min_item_price',
419         'Minimum Item Price',
420         'coust', 'label'),
421     oils_i18n_gettext(
422         'circ.min_item_price',
423         'When charging for lost items, charge this amount as a minimum.',
424         'coust', 'description'),
425     'currency',
426     NULL
427 ), (
428     'circ.max_item_price',
429     'finance',
430     oils_i18n_gettext(
431         'circ.max_item_price',
432         'Maximum Item Price',
433         'coust', 'label'),
434     oils_i18n_gettext(
435         'circ.max_item_price',
436         'When charging for lost items, limit the charge to this as a maximum.',
437         'coust', 'description'),
438     'currency',
439     NULL
440 );
441
442 -- Compiled list of all changed functions and views where we went from:
443 --   array_accum() to array_agg()
444 --   array_to_string(array_agg()) to string_agg()
445
446
447 SELECT evergreen.upgrade_deps_block_check('0855', :eg_version);
448
449 -- from 000.functions.general.sql
450
451
452 -- from 002.functions.config.sql
453
454 CREATE OR REPLACE FUNCTION public.extract_marc_field ( TEXT, BIGINT, TEXT, TEXT ) RETURNS TEXT AS $$
455     SELECT regexp_replace(string_agg(output,' '),$4,'','g') FROM oils_xpath_table('id', 'marc', $1, $3, 'id='||$2)x(id INT, output TEXT);
456 $$ LANGUAGE SQL;
457
458
459 -- from 011.schema.authority.sql
460
461 CREATE OR REPLACE FUNCTION authority.axis_authority_tags(a TEXT) RETURNS INT[] AS $$
462     SELECT ARRAY_AGG(field) FROM authority.browse_axis_authority_field_map WHERE axis = $1;
463 $$ LANGUAGE SQL;
464
465 CREATE OR REPLACE FUNCTION authority.axis_authority_tags_refs(a TEXT) RETURNS INT[] AS $$
466     SELECT ARRAY_AGG(y) from (
467        SELECT  unnest(ARRAY_CAT(
468                  ARRAY[a.field],
469                  (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.field)
470              )) y
471        FROM  authority.browse_axis_authority_field_map a
472        WHERE axis = $1) x
473 $$ LANGUAGE SQL;
474
475 CREATE OR REPLACE FUNCTION authority.btag_authority_tags(btag TEXT) RETURNS INT[] AS $$
476     SELECT ARRAY_AGG(authority_field) FROM authority.control_set_bib_field WHERE tag = $1
477 $$ LANGUAGE SQL;
478
479 CREATE OR REPLACE FUNCTION authority.btag_authority_tags_refs(btag TEXT) RETURNS INT[] AS $$
480     SELECT ARRAY_AGG(y) from (
481         SELECT  unnest(ARRAY_CAT(
482                     ARRAY[a.authority_field],
483                     (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.authority_field)
484                 )) y
485       FROM  authority.control_set_bib_field a
486       WHERE a.tag = $1) x
487 $$ LANGUAGE SQL;
488
489 CREATE OR REPLACE FUNCTION authority.atag_authority_tags(atag TEXT) RETURNS INT[] AS $$
490     SELECT ARRAY_AGG(id) FROM authority.control_set_authority_field WHERE tag = $1
491 $$ LANGUAGE SQL;
492
493 CREATE OR REPLACE FUNCTION authority.atag_authority_tags_refs(atag TEXT) RETURNS INT[] AS $$
494     SELECT ARRAY_AGG(y) from (
495         SELECT  unnest(ARRAY_CAT(
496                     ARRAY[a.id],
497                     (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.id)
498                 )) y
499       FROM  authority.control_set_authority_field a
500       WHERE a.tag = $1) x
501 $$ LANGUAGE SQL;
502
503
504 -- from 012.schema.vandelay.sql
505
506 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT, attr_defs TEXT[]) RETURNS hstore AS $_$
507 DECLARE
508     transformed_xml TEXT;
509     prev_xfrm       TEXT;
510     normalizer      RECORD;
511     xfrm            config.xml_transform%ROWTYPE;
512     attr_value      TEXT;
513     new_attrs       HSTORE := ''::HSTORE;
514     attr_def        config.record_attr_definition%ROWTYPE;
515 BEGIN
516
517     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE name IN (SELECT * FROM UNNEST(attr_defs)) ORDER BY format LOOP
518
519         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
520             SELECT  STRING_AGG(x.value, COALESCE(attr_def.joiner,' ')) INTO attr_value
521               FROM  vandelay.flatten_marc(xml) AS x
522               WHERE x.tag LIKE attr_def.tag
523                     AND CASE
524                         WHEN attr_def.sf_list IS NOT NULL
525                             THEN POSITION(x.subfield IN attr_def.sf_list) > 0
526                         ELSE TRUE
527                         END
528               GROUP BY x.tag
529               ORDER BY x.tag
530               LIMIT 1;
531
532         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
533             attr_value := vandelay.marc21_extract_fixed_field(xml, attr_def.fixed_field);
534
535         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
536
537             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
538
539             -- See if we can skip the XSLT ... it's expensive
540             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
541                 -- Can't skip the transform
542                 IF xfrm.xslt <> '---' THEN
543                     transformed_xml := oils_xslt_process(xml,xfrm.xslt);
544                 ELSE
545                     transformed_xml := xml;
546                 END IF;
547
548                 prev_xfrm := xfrm.name;
549             END IF;
550
551             IF xfrm.name IS NULL THEN
552                 -- just grab the marcxml (empty) transform
553                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
554                 prev_xfrm := xfrm.name;
555             END IF;
556
557             attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
558
559         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
560             SELECT  m.value::TEXT INTO attr_value
561               FROM  vandelay.marc21_physical_characteristics(xml) v
562                     JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
563               WHERE v.subfield = attr_def.phys_char_sf
564               LIMIT 1; -- Just in case ...
565
566         END IF;
567
568         -- apply index normalizers to attr_value
569         FOR normalizer IN
570             SELECT  n.func AS func,
571                     n.param_count AS param_count,
572                     m.params AS params
573               FROM  config.index_normalizer n
574                     JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
575               WHERE attr = attr_def.name
576               ORDER BY m.pos LOOP
577                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
578                     quote_nullable( attr_value ) ||
579                     CASE
580                         WHEN normalizer.param_count > 0
581                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
582                             ELSE ''
583                         END ||
584                     ')' INTO attr_value;
585
586         END LOOP;
587
588         -- Add the new value to the hstore
589         new_attrs := new_attrs || hstore( attr_def.name, attr_value );
590
591     END LOOP;
592
593     RETURN new_attrs;
594 END;
595 $_$ LANGUAGE PLPGSQL;
596
597 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT ) RETURNS hstore AS $_$
598     SELECT vandelay.extract_rec_attrs( $1, (SELECT ARRAY_AGG(name) FROM config.record_attr_definition));
599 $_$ LANGUAGE SQL;
600
601 CREATE OR REPLACE FUNCTION vandelay.match_set_test_marcxml(
602     match_set_id INTEGER, record_xml TEXT, bucket_id INTEGER 
603 ) RETURNS SETOF vandelay.match_set_test_result AS $$
604 DECLARE
605     tags_rstore HSTORE;
606     svf_rstore  HSTORE;
607     coal        TEXT;
608     joins       TEXT;
609     query_      TEXT;
610     wq          TEXT;
611     qvalue      INTEGER;
612     rec         RECORD;
613 BEGIN
614     tags_rstore := vandelay.flatten_marc_hstore(record_xml);
615     svf_rstore := vandelay.extract_rec_attrs(record_xml);
616
617     CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
618     CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
619
620     -- generate the where clause and return that directly (into wq), and as
621     -- a side-effect, populate the _vandelay_tmp_[qj]rows tables.
622     wq := vandelay.get_expr_from_match_set(match_set_id, tags_rstore);
623
624     query_ := 'SELECT DISTINCT(record), ';
625
626     -- qrows table is for the quality bits we add to the SELECT clause
627     SELECT STRING_AGG(
628         'COALESCE(n' || q::TEXT || '.quality, 0)', ' + '
629     ) INTO coal FROM _vandelay_tmp_qrows;
630
631     -- our query string so far is the SELECT clause and the inital FROM.
632     -- no JOINs yet nor the WHERE clause
633     query_ := query_ || coal || ' AS quality ' || E'\n';
634
635     -- jrows table is for the joins we must make (and the real text conditions)
636     SELECT STRING_AGG(j, E'\n') INTO joins
637         FROM _vandelay_tmp_jrows;
638
639     -- add those joins and the where clause to our query.
640     query_ := query_ || joins || E'\n';
641
642     -- join the record bucket
643     IF bucket_id IS NOT NULL THEN
644         query_ := query_ || 'JOIN container.biblio_record_entry_bucket_item ' ||
645             'brebi ON (brebi.target_biblio_record_entry = record ' ||
646             'AND brebi.bucket = ' || bucket_id || E')\n';
647     END IF;
648
649     query_ := query_ || 'JOIN biblio.record_entry bre ON (bre.id = record) ' || 'WHERE ' || wq || ' AND not bre.deleted';
650
651     -- this will return rows of record,quality
652     FOR rec IN EXECUTE query_ USING tags_rstore, svf_rstore LOOP
653         RETURN NEXT rec;
654     END LOOP;
655
656     DROP TABLE _vandelay_tmp_qrows;
657     DROP TABLE _vandelay_tmp_jrows;
658     RETURN;
659 END;
660 $$ LANGUAGE PLPGSQL;
661
662 CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
663     record_xml TEXT
664 ) RETURNS HSTORE AS $func$
665 BEGIN
666     RETURN (SELECT
667         HSTORE(
668             ARRAY_AGG(tag || (COALESCE(subfield, ''))),
669             ARRAY_AGG(value)
670         )
671         FROM (
672             SELECT  tag, subfield, ARRAY_AGG(value)::TEXT AS value
673               FROM  (SELECT tag,
674                             subfield,
675                             CASE WHEN tag = '020' THEN -- caseless -- isbn
676                                 LOWER((REGEXP_MATCHES(value,$$^(\S{10,17})$$))[1] || '%')
677                             WHEN tag = '022' THEN -- caseless -- issn
678                                 LOWER((REGEXP_MATCHES(value,$$^(\S{4}[- ]?\S{4})$$))[1] || '%')
679                             WHEN tag = '024' THEN -- caseless -- upc (other)
680                                 LOWER(value || '%')
681                             ELSE
682                                 value
683                             END AS value
684                       FROM  vandelay.flatten_marc(record_xml)) x
685                 GROUP BY tag, subfield ORDER BY tag, subfield
686         ) subquery
687     );
688 END;
689 $func$ LANGUAGE PLPGSQL;
690
691 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set_point(
692     node vandelay.match_set_point,
693     tags_rstore HSTORE
694 ) RETURNS TEXT AS $$
695 DECLARE
696     q           TEXT;
697     i           INTEGER;
698     this_op     TEXT;
699     children    INTEGER[];
700     child       vandelay.match_set_point;
701 BEGIN
702     SELECT ARRAY_AGG(id) INTO children FROM vandelay.match_set_point
703         WHERE parent = node.id;
704
705     IF ARRAY_LENGTH(children, 1) > 0 THEN
706         this_op := vandelay._get_expr_render_one(node);
707         q := '(';
708         i := 1;
709         WHILE children[i] IS NOT NULL LOOP
710             SELECT * INTO child FROM vandelay.match_set_point
711                 WHERE id = children[i];
712             IF i > 1 THEN
713                 q := q || ' ' || this_op || ' ';
714             END IF;
715             i := i + 1;
716             q := q || vandelay.get_expr_from_match_set_point(child, tags_rstore);
717         END LOOP;
718         q := q || ')';
719         RETURN q;
720     ELSIF node.bool_op IS NULL THEN
721         PERFORM vandelay._get_expr_push_qrow(node);
722         PERFORM vandelay._get_expr_push_jrow(node, tags_rstore);
723         RETURN vandelay._get_expr_render_one(node);
724     ELSE
725         RETURN '';
726     END IF;
727 END;
728 $$  LANGUAGE PLPGSQL;
729
730
731 -- from 030.schema.metabib.sql
732
733 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
734 DECLARE
735     uris            TEXT[];
736     uri_xml         TEXT;
737     uri_label       TEXT;
738     uri_href        TEXT;
739     uri_use         TEXT;
740     uri_owner_list  TEXT[];
741     uri_owner       TEXT;
742     uri_owner_id    INT;
743     uri_id          INT;
744     uri_cn_id       INT;
745     uri_map_id      INT;
746 BEGIN
747
748     -- Clear any URI mappings and call numbers for this bib.
749     -- This leads to acn / auricnm inflation, but also enables
750     -- old acn/auricnm's to go away and for bibs to be deleted.
751     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
752         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
753         DELETE FROM asset.call_number WHERE id = uri_cn_id;
754     END LOOP;
755
756     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
757     IF ARRAY_UPPER(uris,1) > 0 THEN
758         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
759             -- First we pull info out of the 856
760             uri_xml     := uris[i];
761
762             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
763             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()',uri_xml))[1];
764             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
765
766             IF uri_label IS NULL THEN
767                 uri_label := uri_href;
768             END IF;
769             CONTINUE WHEN uri_href IS NULL;
770
771             -- Get the distinct list of libraries wanting to use 
772             SELECT  ARRAY_AGG(
773                         DISTINCT REGEXP_REPLACE(
774                             x,
775                             $re$^.*?\((\w+)\).*$$re$,
776                             E'\\1'
777                         )
778                     ) INTO uri_owner_list
779               FROM  UNNEST(
780                         oils_xpath(
781                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
782                             uri_xml
783                         )
784                     )x;
785
786             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
787
788                 -- look for a matching uri
789                 IF uri_use IS NULL THEN
790                     SELECT id INTO uri_id
791                         FROM asset.uri
792                         WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active
793                         ORDER BY id LIMIT 1;
794                     IF NOT FOUND THEN -- create one
795                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
796                         SELECT id INTO uri_id
797                             FROM asset.uri
798                             WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
799                     END IF;
800                 ELSE
801                     SELECT id INTO uri_id
802                         FROM asset.uri
803                         WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active
804                         ORDER BY id LIMIT 1;
805                     IF NOT FOUND THEN -- create one
806                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
807                         SELECT id INTO uri_id
808                             FROM asset.uri
809                             WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
810                     END IF;
811                 END IF;
812
813                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
814                     uri_owner := uri_owner_list[j];
815
816                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
817                     CONTINUE WHEN NOT FOUND;
818
819                     -- we need a call number to link through
820                     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;
821                     IF NOT FOUND THEN
822                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
823                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
824                         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;
825                     END IF;
826
827                     -- now, link them if they're not already
828                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
829                     IF NOT FOUND THEN
830                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
831                     END IF;
832
833                 END LOOP;
834
835             END IF;
836
837         END LOOP;
838     END IF;
839
840     RETURN;
841 END;
842 $func$ LANGUAGE PLPGSQL;
843
844 -- from 100.circ_matrix.sql
845
846 CREATE OR REPLACE FUNCTION actor.calculate_system_penalties( match_user INT, context_org INT ) RETURNS SETOF actor.usr_standing_penalty AS $func$
847 DECLARE
848     user_object         actor.usr%ROWTYPE;
849     new_sp_row          actor.usr_standing_penalty%ROWTYPE;
850     existing_sp_row     actor.usr_standing_penalty%ROWTYPE;
851     collections_fines   permission.grp_penalty_threshold%ROWTYPE;
852     max_fines           permission.grp_penalty_threshold%ROWTYPE;
853     max_overdue         permission.grp_penalty_threshold%ROWTYPE;
854     max_items_out       permission.grp_penalty_threshold%ROWTYPE;
855     max_lost            permission.grp_penalty_threshold%ROWTYPE;
856     max_longoverdue     permission.grp_penalty_threshold%ROWTYPE;
857     tmp_grp             INT;
858     items_overdue       INT;
859     items_out           INT;
860     items_lost          INT;
861     items_longoverdue   INT;
862     context_org_list    INT[];
863     current_fines        NUMERIC(8,2) := 0.0;
864     tmp_fines            NUMERIC(8,2);
865     tmp_groc            RECORD;
866     tmp_circ            RECORD;
867     tmp_org             actor.org_unit%ROWTYPE;
868     tmp_penalty         config.standing_penalty%ROWTYPE;
869     tmp_depth           INTEGER;
870 BEGIN
871     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
872
873     -- Max fines
874     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
875
876     -- Fail if the user has a high fine balance
877     LOOP
878         tmp_grp := user_object.profile;
879         LOOP
880             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 1 AND org_unit = tmp_org.id;
881
882             IF max_fines.threshold IS NULL THEN
883                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
884             ELSE
885                 EXIT;
886             END IF;
887
888             IF tmp_grp IS NULL THEN
889                 EXIT;
890             END IF;
891         END LOOP;
892
893         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
894             EXIT;
895         END IF;
896
897         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
898
899     END LOOP;
900
901     IF max_fines.threshold IS NOT NULL THEN
902
903         RETURN QUERY
904             SELECT  *
905               FROM  actor.usr_standing_penalty
906               WHERE usr = match_user
907                     AND org_unit = max_fines.org_unit
908                     AND (stop_date IS NULL or stop_date > NOW())
909                     AND standing_penalty = 1;
910
911         SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( max_fines.org_unit );
912
913         SELECT  SUM(f.balance_owed) INTO current_fines
914           FROM  money.materialized_billable_xact_summary f
915                 JOIN (
916                     SELECT  r.id
917                       FROM  booking.reservation r
918                       WHERE r.usr = match_user
919                             AND r.pickup_lib IN (SELECT * FROM unnest(context_org_list))
920                             AND xact_finish IS NULL
921                                 UNION ALL
922                     SELECT  g.id
923                       FROM  money.grocery g
924                       WHERE g.usr = match_user
925                             AND g.billing_location IN (SELECT * FROM unnest(context_org_list))
926                             AND xact_finish IS NULL
927                                 UNION ALL
928                     SELECT  circ.id
929                       FROM  action.circulation circ
930                       WHERE circ.usr = match_user
931                             AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
932                             AND xact_finish IS NULL ) l USING (id);
933
934         IF current_fines >= max_fines.threshold THEN
935             new_sp_row.usr := match_user;
936             new_sp_row.org_unit := max_fines.org_unit;
937             new_sp_row.standing_penalty := 1;
938             RETURN NEXT new_sp_row;
939         END IF;
940     END IF;
941
942     -- Start over for max overdue
943     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
944
945     -- Fail if the user has too many overdue items
946     LOOP
947         tmp_grp := user_object.profile;
948         LOOP
949
950             SELECT * INTO max_overdue FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 2 AND org_unit = tmp_org.id;
951
952             IF max_overdue.threshold IS NULL THEN
953                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
954             ELSE
955                 EXIT;
956             END IF;
957
958             IF tmp_grp IS NULL THEN
959                 EXIT;
960             END IF;
961         END LOOP;
962
963         IF max_overdue.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
964             EXIT;
965         END IF;
966
967         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
968
969     END LOOP;
970
971     IF max_overdue.threshold IS NOT NULL THEN
972
973         RETURN QUERY
974             SELECT  *
975               FROM  actor.usr_standing_penalty
976               WHERE usr = match_user
977                     AND org_unit = max_overdue.org_unit
978                     AND (stop_date IS NULL or stop_date > NOW())
979                     AND standing_penalty = 2;
980
981         SELECT  INTO items_overdue COUNT(*)
982           FROM  action.circulation circ
983                 JOIN  actor.org_unit_full_path( max_overdue.org_unit ) fp ON (circ.circ_lib = fp.id)
984           WHERE circ.usr = match_user
985             AND circ.checkin_time IS NULL
986             AND circ.due_date < NOW()
987             AND (circ.stop_fines = 'MAXFINES' OR circ.stop_fines IS NULL);
988
989         IF items_overdue >= max_overdue.threshold::INT THEN
990             new_sp_row.usr := match_user;
991             new_sp_row.org_unit := max_overdue.org_unit;
992             new_sp_row.standing_penalty := 2;
993             RETURN NEXT new_sp_row;
994         END IF;
995     END IF;
996
997     -- Start over for max out
998     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
999
1000     -- Fail if the user has too many checked out items
1001     LOOP
1002         tmp_grp := user_object.profile;
1003         LOOP
1004             SELECT * INTO max_items_out FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 3 AND org_unit = tmp_org.id;
1005
1006             IF max_items_out.threshold IS NULL THEN
1007                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
1008             ELSE
1009                 EXIT;
1010             END IF;
1011
1012             IF tmp_grp IS NULL THEN
1013                 EXIT;
1014             END IF;
1015         END LOOP;
1016
1017         IF max_items_out.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
1018             EXIT;
1019         END IF;
1020
1021         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1022
1023     END LOOP;
1024
1025
1026     -- Fail if the user has too many items checked out
1027     IF max_items_out.threshold IS NOT NULL THEN
1028
1029         RETURN QUERY
1030             SELECT  *
1031               FROM  actor.usr_standing_penalty
1032               WHERE usr = match_user
1033                     AND org_unit = max_items_out.org_unit
1034                     AND (stop_date IS NULL or stop_date > NOW())
1035                     AND standing_penalty = 3;
1036
1037         SELECT  INTO items_out COUNT(*)
1038           FROM  action.circulation circ
1039                 JOIN  actor.org_unit_full_path( max_items_out.org_unit ) fp ON (circ.circ_lib = fp.id)
1040           WHERE circ.usr = match_user
1041                 AND circ.checkin_time IS NULL
1042                 AND (circ.stop_fines IN (
1043                     SELECT 'MAXFINES'::TEXT
1044                     UNION ALL
1045                     SELECT 'LONGOVERDUE'::TEXT
1046                     UNION ALL
1047                     SELECT 'LOST'::TEXT
1048                     WHERE 'true' ILIKE
1049                     (
1050                         SELECT CASE
1051                             WHEN (SELECT value FROM actor.org_unit_ancestor_setting('circ.tally_lost', circ.circ_lib)) ILIKE 'true' THEN 'true'
1052                             ELSE 'false'
1053                         END
1054                     )
1055                     UNION ALL
1056                     SELECT 'CLAIMSRETURNED'::TEXT
1057                     WHERE 'false' ILIKE
1058                     (
1059                         SELECT CASE
1060                             WHEN (SELECT value FROM actor.org_unit_ancestor_setting('circ.do_not_tally_claims_returned', circ.circ_lib)) ILIKE 'true' THEN 'true'
1061                             ELSE 'false'
1062                         END
1063                     )
1064                     ) OR circ.stop_fines IS NULL)
1065                 AND xact_finish IS NULL;
1066
1067            IF items_out >= max_items_out.threshold::INT THEN
1068             new_sp_row.usr := match_user;
1069             new_sp_row.org_unit := max_items_out.org_unit;
1070             new_sp_row.standing_penalty := 3;
1071             RETURN NEXT new_sp_row;
1072            END IF;
1073     END IF;
1074
1075     -- Start over for max lost
1076     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1077
1078     -- Fail if the user has too many lost items
1079     LOOP
1080         tmp_grp := user_object.profile;
1081         LOOP
1082
1083             SELECT * INTO max_lost FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 5 AND org_unit = tmp_org.id;
1084
1085             IF max_lost.threshold IS NULL THEN
1086                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
1087             ELSE
1088                 EXIT;
1089             END IF;
1090
1091             IF tmp_grp IS NULL THEN
1092                 EXIT;
1093             END IF;
1094         END LOOP;
1095
1096         IF max_lost.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
1097             EXIT;
1098         END IF;
1099
1100         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1101
1102     END LOOP;
1103
1104     IF max_lost.threshold IS NOT NULL THEN
1105
1106         RETURN QUERY
1107             SELECT  *
1108             FROM  actor.usr_standing_penalty
1109             WHERE usr = match_user
1110                 AND org_unit = max_lost.org_unit
1111                 AND (stop_date IS NULL or stop_date > NOW())
1112                 AND standing_penalty = 5;
1113
1114         SELECT  INTO items_lost COUNT(*)
1115         FROM  action.circulation circ
1116             JOIN  actor.org_unit_full_path( max_lost.org_unit ) fp ON (circ.circ_lib = fp.id)
1117         WHERE circ.usr = match_user
1118             AND circ.checkin_time IS NULL
1119             AND (circ.stop_fines = 'LOST')
1120             AND xact_finish IS NULL;
1121
1122         IF items_lost >= max_lost.threshold::INT AND 0 < max_lost.threshold::INT THEN
1123             new_sp_row.usr := match_user;
1124             new_sp_row.org_unit := max_lost.org_unit;
1125             new_sp_row.standing_penalty := 5;
1126             RETURN NEXT new_sp_row;
1127         END IF;
1128     END IF;
1129
1130     -- Start over for max longoverdue
1131     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1132
1133     -- Fail if the user has too many longoverdue items
1134     LOOP
1135         tmp_grp := user_object.profile;
1136         LOOP
1137
1138             SELECT * INTO max_longoverdue 
1139                 FROM permission.grp_penalty_threshold 
1140                 WHERE grp = tmp_grp AND 
1141                     penalty = 35 AND 
1142                     org_unit = tmp_org.id;
1143
1144             IF max_longoverdue.threshold IS NULL THEN
1145                 SELECT parent INTO tmp_grp 
1146                     FROM permission.grp_tree WHERE id = tmp_grp;
1147             ELSE
1148                 EXIT;
1149             END IF;
1150
1151             IF tmp_grp IS NULL THEN
1152                 EXIT;
1153             END IF;
1154         END LOOP;
1155
1156         IF max_longoverdue.threshold IS NOT NULL 
1157                 OR tmp_org.parent_ou IS NULL THEN
1158             EXIT;
1159         END IF;
1160
1161         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1162
1163     END LOOP;
1164
1165     IF max_longoverdue.threshold IS NOT NULL THEN
1166
1167         RETURN QUERY
1168             SELECT  *
1169             FROM  actor.usr_standing_penalty
1170             WHERE usr = match_user
1171                 AND org_unit = max_longoverdue.org_unit
1172                 AND (stop_date IS NULL or stop_date > NOW())
1173                 AND standing_penalty = 35;
1174
1175         SELECT INTO items_longoverdue COUNT(*)
1176         FROM action.circulation circ
1177             JOIN actor.org_unit_full_path( max_longoverdue.org_unit ) fp 
1178                 ON (circ.circ_lib = fp.id)
1179         WHERE circ.usr = match_user
1180             AND circ.checkin_time IS NULL
1181             AND (circ.stop_fines = 'LONGOVERDUE')
1182             AND xact_finish IS NULL;
1183
1184         IF items_longoverdue >= max_longoverdue.threshold::INT 
1185                 AND 0 < max_longoverdue.threshold::INT THEN
1186             new_sp_row.usr := match_user;
1187             new_sp_row.org_unit := max_longoverdue.org_unit;
1188             new_sp_row.standing_penalty := 35;
1189             RETURN NEXT new_sp_row;
1190         END IF;
1191     END IF;
1192
1193
1194     -- Start over for collections warning
1195     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1196
1197     -- Fail if the user has a collections-level fine balance
1198     LOOP
1199         tmp_grp := user_object.profile;
1200         LOOP
1201             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 4 AND org_unit = tmp_org.id;
1202
1203             IF max_fines.threshold IS NULL THEN
1204                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
1205             ELSE
1206                 EXIT;
1207             END IF;
1208
1209             IF tmp_grp IS NULL THEN
1210                 EXIT;
1211             END IF;
1212         END LOOP;
1213
1214         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
1215             EXIT;
1216         END IF;
1217
1218         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1219
1220     END LOOP;
1221
1222     IF max_fines.threshold IS NOT NULL THEN
1223
1224         RETURN QUERY
1225             SELECT  *
1226               FROM  actor.usr_standing_penalty
1227               WHERE usr = match_user
1228                     AND org_unit = max_fines.org_unit
1229                     AND (stop_date IS NULL or stop_date > NOW())
1230                     AND standing_penalty = 4;
1231
1232         SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( max_fines.org_unit );
1233
1234         SELECT  SUM(f.balance_owed) INTO current_fines
1235           FROM  money.materialized_billable_xact_summary f
1236                 JOIN (
1237                     SELECT  r.id
1238                       FROM  booking.reservation r
1239                       WHERE r.usr = match_user
1240                             AND r.pickup_lib IN (SELECT * FROM unnest(context_org_list))
1241                             AND r.xact_finish IS NULL
1242                                 UNION ALL
1243                     SELECT  g.id
1244                       FROM  money.grocery g
1245                       WHERE g.usr = match_user
1246                             AND g.billing_location IN (SELECT * FROM unnest(context_org_list))
1247                             AND g.xact_finish IS NULL
1248                                 UNION ALL
1249                     SELECT  circ.id
1250                       FROM  action.circulation circ
1251                       WHERE circ.usr = match_user
1252                             AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
1253                             AND circ.xact_finish IS NULL ) l USING (id);
1254
1255         IF current_fines >= max_fines.threshold THEN
1256             new_sp_row.usr := match_user;
1257             new_sp_row.org_unit := max_fines.org_unit;
1258             new_sp_row.standing_penalty := 4;
1259             RETURN NEXT new_sp_row;
1260         END IF;
1261     END IF;
1262
1263     -- Start over for in collections
1264     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1265
1266     -- Remove the in-collections penalty if the user has paid down enough
1267     -- This penalty is different, because this code is not responsible for creating 
1268     -- new in-collections penalties, only for removing them
1269     LOOP
1270         tmp_grp := user_object.profile;
1271         LOOP
1272             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 30 AND org_unit = tmp_org.id;
1273
1274             IF max_fines.threshold IS NULL THEN
1275                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
1276             ELSE
1277                 EXIT;
1278             END IF;
1279
1280             IF tmp_grp IS NULL THEN
1281                 EXIT;
1282             END IF;
1283         END LOOP;
1284
1285         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
1286             EXIT;
1287         END IF;
1288
1289         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1290
1291     END LOOP;
1292
1293     IF max_fines.threshold IS NOT NULL THEN
1294
1295         SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( max_fines.org_unit );
1296
1297         -- first, see if the user had paid down to the threshold
1298         SELECT  SUM(f.balance_owed) INTO current_fines
1299           FROM  money.materialized_billable_xact_summary f
1300                 JOIN (
1301                     SELECT  r.id
1302                       FROM  booking.reservation r
1303                       WHERE r.usr = match_user
1304                             AND r.pickup_lib IN (SELECT * FROM unnest(context_org_list))
1305                             AND r.xact_finish IS NULL
1306                                 UNION ALL
1307                     SELECT  g.id
1308                       FROM  money.grocery g
1309                       WHERE g.usr = match_user
1310                             AND g.billing_location IN (SELECT * FROM unnest(context_org_list))
1311                             AND g.xact_finish IS NULL
1312                                 UNION ALL
1313                     SELECT  circ.id
1314                       FROM  action.circulation circ
1315                       WHERE circ.usr = match_user
1316                             AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
1317                             AND circ.xact_finish IS NULL ) l USING (id);
1318
1319         IF current_fines IS NULL OR current_fines <= max_fines.threshold THEN
1320             -- patron has paid down enough
1321
1322             SELECT INTO tmp_penalty * FROM config.standing_penalty WHERE id = 30;
1323
1324             IF tmp_penalty.org_depth IS NOT NULL THEN
1325
1326                 -- since this code is not responsible for applying the penalty, it can't 
1327                 -- guarantee the current context org will match the org at which the penalty 
1328                 --- was applied.  search up the org tree until we hit the configured penalty depth
1329                 SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1330                 SELECT INTO tmp_depth depth FROM actor.org_unit_type WHERE id = tmp_org.ou_type;
1331
1332                 WHILE tmp_depth >= tmp_penalty.org_depth LOOP
1333
1334                     RETURN QUERY
1335                         SELECT  *
1336                           FROM  actor.usr_standing_penalty
1337                           WHERE usr = match_user
1338                                 AND org_unit = tmp_org.id
1339                                 AND (stop_date IS NULL or stop_date > NOW())
1340                                 AND standing_penalty = 30;
1341
1342                     IF tmp_org.parent_ou IS NULL THEN
1343                         EXIT;
1344                     END IF;
1345
1346                     SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1347                     SELECT INTO tmp_depth depth FROM actor.org_unit_type WHERE id = tmp_org.ou_type;
1348                 END LOOP;
1349
1350             ELSE
1351
1352                 -- no penalty depth is defined, look for exact matches
1353
1354                 RETURN QUERY
1355                     SELECT  *
1356                       FROM  actor.usr_standing_penalty
1357                       WHERE usr = match_user
1358                             AND org_unit = max_fines.org_unit
1359                             AND (stop_date IS NULL or stop_date > NOW())
1360                             AND standing_penalty = 30;
1361             END IF;
1362     
1363         END IF;
1364
1365     END IF;
1366
1367     RETURN;
1368 END;
1369 $func$ LANGUAGE plpgsql;
1370
1371
1372 -- from 110.hold_matrix.sql
1373
1374 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$
1375 DECLARE
1376     matchpoint_id        INT;
1377     user_object        actor.usr%ROWTYPE;
1378     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
1379     standing_penalty    config.standing_penalty%ROWTYPE;
1380     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
1381     transit_source        actor.org_unit%ROWTYPE;
1382     item_object        asset.copy%ROWTYPE;
1383     item_cn_object     asset.call_number%ROWTYPE;
1384     item_status_object  config.copy_status%ROWTYPE;
1385     item_location_object    asset.copy_location%ROWTYPE;
1386     ou_skip              actor.org_unit_setting%ROWTYPE;
1387     result            action.matrix_test_result;
1388     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
1389     use_active_date   TEXT;
1390     age_protect_date  TIMESTAMP WITH TIME ZONE;
1391     hold_count        INT;
1392     hold_transit_prox    INT;
1393     frozen_hold_count    INT;
1394     context_org_list    INT[];
1395     done            BOOL := FALSE;
1396     hold_penalty TEXT;
1397 BEGIN
1398     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
1399     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( pickup_ou );
1400
1401     result.success := TRUE;
1402
1403     -- The HOLD penalty block only applies to new holds.
1404     -- The CAPTURE penalty block applies to existing holds.
1405     hold_penalty := 'HOLD';
1406     IF retargetting THEN
1407         hold_penalty := 'CAPTURE';
1408     END IF;
1409
1410     -- Fail if we couldn't find a user
1411     IF user_object.id IS NULL THEN
1412         result.fail_part := 'no_user';
1413         result.success := FALSE;
1414         done := TRUE;
1415         RETURN NEXT result;
1416         RETURN;
1417     END IF;
1418
1419     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
1420
1421     -- Fail if we couldn't find a copy
1422     IF item_object.id IS NULL THEN
1423         result.fail_part := 'no_item';
1424         result.success := FALSE;
1425         done := TRUE;
1426         RETURN NEXT result;
1427         RETURN;
1428     END IF;
1429
1430     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
1431     result.matchpoint := matchpoint_id;
1432
1433     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
1434
1435     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
1436     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
1437         result.fail_part := 'circ.holds.target_skip_me';
1438         result.success := FALSE;
1439         done := TRUE;
1440         RETURN NEXT result;
1441         RETURN;
1442     END IF;
1443
1444     -- Fail if user is barred
1445     IF user_object.barred IS TRUE THEN
1446         result.fail_part := 'actor.usr.barred';
1447         result.success := FALSE;
1448         done := TRUE;
1449         RETURN NEXT result;
1450         RETURN;
1451     END IF;
1452
1453     SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
1454     SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
1455     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
1456
1457     -- Fail if we couldn't find any matchpoint (requires a default)
1458     IF matchpoint_id IS NULL THEN
1459         result.fail_part := 'no_matchpoint';
1460         result.success := FALSE;
1461         done := TRUE;
1462         RETURN NEXT result;
1463         RETURN;
1464     END IF;
1465
1466     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
1467
1468     IF hold_test.holdable IS FALSE THEN
1469         result.fail_part := 'config.hold_matrix_test.holdable';
1470         result.success := FALSE;
1471         done := TRUE;
1472         RETURN NEXT result;
1473     END IF;
1474
1475     IF item_object.holdable IS FALSE THEN
1476         result.fail_part := 'item.holdable';
1477         result.success := FALSE;
1478         done := TRUE;
1479         RETURN NEXT result;
1480     END IF;
1481
1482     IF item_status_object.holdable IS FALSE THEN
1483         result.fail_part := 'status.holdable';
1484         result.success := FALSE;
1485         done := TRUE;
1486         RETURN NEXT result;
1487     END IF;
1488
1489     IF item_location_object.holdable IS FALSE THEN
1490         result.fail_part := 'location.holdable';
1491         result.success := FALSE;
1492         done := TRUE;
1493         RETURN NEXT result;
1494     END IF;
1495
1496     IF hold_test.transit_range IS NOT NULL THEN
1497         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
1498         IF hold_test.distance_is_from_owner THEN
1499             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;
1500         ELSE
1501             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
1502         END IF;
1503
1504         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
1505
1506         IF NOT FOUND THEN
1507             result.fail_part := 'transit_range';
1508             result.success := FALSE;
1509             done := TRUE;
1510             RETURN NEXT result;
1511         END IF;
1512     END IF;
1513  
1514     FOR standing_penalty IN
1515         SELECT  DISTINCT csp.*
1516           FROM  actor.usr_standing_penalty usp
1517                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1518           WHERE usr = match_user
1519                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1520                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1521                 AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
1522
1523         result.fail_part := standing_penalty.name;
1524         result.success := FALSE;
1525         done := TRUE;
1526         RETURN NEXT result;
1527     END LOOP;
1528
1529     IF hold_test.stop_blocked_user IS TRUE THEN
1530         FOR standing_penalty IN
1531             SELECT  DISTINCT csp.*
1532               FROM  actor.usr_standing_penalty usp
1533                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1534               WHERE usr = match_user
1535                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1536                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1537                     AND csp.block_list LIKE '%CIRC%' LOOP
1538     
1539             result.fail_part := standing_penalty.name;
1540             result.success := FALSE;
1541             done := TRUE;
1542             RETURN NEXT result;
1543         END LOOP;
1544     END IF;
1545
1546     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
1547         SELECT    INTO hold_count COUNT(*)
1548           FROM    action.hold_request
1549           WHERE    usr = match_user
1550             AND fulfillment_time IS NULL
1551             AND cancel_time IS NULL
1552             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
1553
1554         IF hold_count >= hold_test.max_holds THEN
1555             result.fail_part := 'config.hold_matrix_test.max_holds';
1556             result.success := FALSE;
1557             done := TRUE;
1558             RETURN NEXT result;
1559         END IF;
1560     END IF;
1561
1562     IF item_object.age_protect IS NOT NULL THEN
1563         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
1564         IF hold_test.distance_is_from_owner THEN
1565             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
1566         ELSE
1567             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
1568         END IF;
1569         IF use_active_date = 'true' THEN
1570             age_protect_date := COALESCE(item_object.active_date, NOW());
1571         ELSE
1572             age_protect_date := item_object.create_date;
1573         END IF;
1574         IF age_protect_date + age_protect_object.age > NOW() THEN
1575             IF hold_test.distance_is_from_owner THEN
1576                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
1577                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
1578             ELSE
1579                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
1580             END IF;
1581
1582             IF hold_transit_prox > age_protect_object.prox THEN
1583                 result.fail_part := 'config.rule_age_hold_protect.prox';
1584                 result.success := FALSE;
1585                 done := TRUE;
1586                 RETURN NEXT result;
1587             END IF;
1588         END IF;
1589     END IF;
1590
1591     IF NOT done THEN
1592         RETURN NEXT result;
1593     END IF;
1594
1595     RETURN;
1596 END;
1597 $func$ LANGUAGE plpgsql;
1598
1599
1600 -- from 300.schema.staged_search.sql
1601
1602
1603 -- from 990.schema.unapi.sql
1604
1605 CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT)
1606 RETURNS anyarray AS $$
1607     SELECT ARRAY_AGG(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2;
1608 $$ LANGUAGE SQL STABLE;
1609
1610
1611 -- from 999.functions.global.sql
1612
1613 CREATE OR REPLACE FUNCTION asset.merge_record_assets( target_record BIGINT, source_record BIGINT ) RETURNS INT AS $func$
1614 DECLARE
1615     moved_objects INT := 0;
1616     source_cn     asset.call_number%ROWTYPE;
1617     target_cn     asset.call_number%ROWTYPE;
1618     metarec       metabib.metarecord%ROWTYPE;
1619     hold          action.hold_request%ROWTYPE;
1620     ser_rec       serial.record_entry%ROWTYPE;
1621     ser_sub       serial.subscription%ROWTYPE;
1622     acq_lineitem  acq.lineitem%ROWTYPE;
1623     acq_request   acq.user_request%ROWTYPE;
1624     booking       booking.resource_type%ROWTYPE;
1625     source_part   biblio.monograph_part%ROWTYPE;
1626     target_part   biblio.monograph_part%ROWTYPE;
1627     multi_home    biblio.peer_bib_copy_map%ROWTYPE;
1628     uri_count     INT := 0;
1629     counter       INT := 0;
1630     uri_datafield TEXT;
1631     uri_text      TEXT := '';
1632 BEGIN
1633
1634     -- move any 856 entries on records that have at least one MARC-mapped URI entry
1635     SELECT  INTO uri_count COUNT(*)
1636       FROM  asset.uri_call_number_map m
1637             JOIN asset.call_number cn ON (m.call_number = cn.id)
1638       WHERE cn.record = source_record;
1639
1640     IF uri_count > 0 THEN
1641         
1642         -- This returns more nodes than you might expect:
1643         -- 7 instead of 1 for an 856 with $u $y $9
1644         SELECT  COUNT(*) INTO counter
1645           FROM  oils_xpath_table(
1646                     'id',
1647                     'marc',
1648                     'biblio.record_entry',
1649                     '//*[@tag="856"]',
1650                     'id=' || source_record
1651                 ) as t(i int,c text);
1652     
1653         FOR i IN 1 .. counter LOOP
1654             SELECT  '<datafield xmlns="http://www.loc.gov/MARC21/slim"' || 
1655                         ' tag="856"' ||
1656                         ' ind1="' || FIRST(ind1) || '"'  ||
1657                         ' ind2="' || FIRST(ind2) || '">' ||
1658                         STRING_AGG(
1659                             '<subfield code="' || subfield || '">' ||
1660                             regexp_replace(
1661                                 regexp_replace(
1662                                     regexp_replace(data,'&','&amp;','g'),
1663                                     '>', '&gt;', 'g'
1664                                 ),
1665                                 '<', '&lt;', 'g'
1666                             ) || '</subfield>', ''
1667                         ) || '</datafield>' INTO uri_datafield
1668               FROM  oils_xpath_table(
1669                         'id',
1670                         'marc',
1671                         'biblio.record_entry',
1672                         '//*[@tag="856"][position()=' || i || ']/@ind1|' ||
1673                         '//*[@tag="856"][position()=' || i || ']/@ind2|' ||
1674                         '//*[@tag="856"][position()=' || i || ']/*/@code|' ||
1675                         '//*[@tag="856"][position()=' || i || ']/*[@code]',
1676                         'id=' || source_record
1677                     ) as t(id int,ind1 text, ind2 text,subfield text,data text);
1678
1679             -- As most of the results will be NULL, protect against NULLifying
1680             -- the valid content that we do generate
1681             uri_text := uri_text || COALESCE(uri_datafield, '');
1682         END LOOP;
1683
1684         IF uri_text <> '' THEN
1685             UPDATE  biblio.record_entry
1686               SET   marc = regexp_replace(marc,'(</[^>]*record>)', uri_text || E'\\1')
1687               WHERE id = target_record;
1688         END IF;
1689
1690     END IF;
1691
1692         -- Find and move metarecords to the target record
1693         SELECT  INTO metarec *
1694           FROM  metabib.metarecord
1695           WHERE master_record = source_record;
1696
1697         IF FOUND THEN
1698                 UPDATE  metabib.metarecord
1699                   SET   master_record = target_record,
1700                         mods = NULL
1701                   WHERE id = metarec.id;
1702
1703                 moved_objects := moved_objects + 1;
1704         END IF;
1705
1706         -- Find call numbers attached to the source ...
1707         FOR source_cn IN SELECT * FROM asset.call_number WHERE record = source_record LOOP
1708
1709                 SELECT  INTO target_cn *
1710                   FROM  asset.call_number
1711                   WHERE label = source_cn.label
1712                         AND owning_lib = source_cn.owning_lib
1713                         AND record = target_record
1714                         AND NOT deleted;
1715
1716                 -- ... and if there's a conflicting one on the target ...
1717                 IF FOUND THEN
1718
1719                         -- ... move the copies to that, and ...
1720                         UPDATE  asset.copy
1721                           SET   call_number = target_cn.id
1722                           WHERE call_number = source_cn.id;
1723
1724                         -- ... move V holds to the move-target call number
1725                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_cn.id AND hold_type = 'V' LOOP
1726                 
1727                                 UPDATE  action.hold_request
1728                                   SET   target = target_cn.id
1729                                   WHERE id = hold.id;
1730                 
1731                                 moved_objects := moved_objects + 1;
1732                         END LOOP;
1733
1734                 -- ... if not ...
1735                 ELSE
1736                         -- ... just move the call number to the target record
1737                         UPDATE  asset.call_number
1738                           SET   record = target_record
1739                           WHERE id = source_cn.id;
1740                 END IF;
1741
1742                 moved_objects := moved_objects + 1;
1743         END LOOP;
1744
1745         -- Find T holds targeting the source record ...
1746         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_record AND hold_type = 'T' LOOP
1747
1748                 -- ... and move them to the target record
1749                 UPDATE  action.hold_request
1750                   SET   target = target_record
1751                   WHERE id = hold.id;
1752
1753                 moved_objects := moved_objects + 1;
1754         END LOOP;
1755
1756         -- Find serial records targeting the source record ...
1757         FOR ser_rec IN SELECT * FROM serial.record_entry WHERE record = source_record LOOP
1758                 -- ... and move them to the target record
1759                 UPDATE  serial.record_entry
1760                   SET   record = target_record
1761                   WHERE id = ser_rec.id;
1762
1763                 moved_objects := moved_objects + 1;
1764         END LOOP;
1765
1766         -- Find serial subscriptions targeting the source record ...
1767         FOR ser_sub IN SELECT * FROM serial.subscription WHERE record_entry = source_record LOOP
1768                 -- ... and move them to the target record
1769                 UPDATE  serial.subscription
1770                   SET   record_entry = target_record
1771                   WHERE id = ser_sub.id;
1772
1773                 moved_objects := moved_objects + 1;
1774         END LOOP;
1775
1776         -- Find booking resource types targeting the source record ...
1777         FOR booking IN SELECT * FROM booking.resource_type WHERE record = source_record LOOP
1778                 -- ... and move them to the target record
1779                 UPDATE  booking.resource_type
1780                   SET   record = target_record
1781                   WHERE id = booking.id;
1782
1783                 moved_objects := moved_objects + 1;
1784         END LOOP;
1785
1786         -- Find acq lineitems targeting the source record ...
1787         FOR acq_lineitem IN SELECT * FROM acq.lineitem WHERE eg_bib_id = source_record LOOP
1788                 -- ... and move them to the target record
1789                 UPDATE  acq.lineitem
1790                   SET   eg_bib_id = target_record
1791                   WHERE id = acq_lineitem.id;
1792
1793                 moved_objects := moved_objects + 1;
1794         END LOOP;
1795
1796         -- Find acq user purchase requests targeting the source record ...
1797         FOR acq_request IN SELECT * FROM acq.user_request WHERE eg_bib = source_record LOOP
1798                 -- ... and move them to the target record
1799                 UPDATE  acq.user_request
1800                   SET   eg_bib = target_record
1801                   WHERE id = acq_request.id;
1802
1803                 moved_objects := moved_objects + 1;
1804         END LOOP;
1805
1806         -- Find parts attached to the source ...
1807         FOR source_part IN SELECT * FROM biblio.monograph_part WHERE record = source_record LOOP
1808
1809                 SELECT  INTO target_part *
1810                   FROM  biblio.monograph_part
1811                   WHERE label = source_part.label
1812                         AND record = target_record;
1813
1814                 -- ... and if there's a conflicting one on the target ...
1815                 IF FOUND THEN
1816
1817                         -- ... move the copy-part maps to that, and ...
1818                         UPDATE  asset.copy_part_map
1819                           SET   part = target_part.id
1820                           WHERE part = source_part.id;
1821
1822                         -- ... move P holds to the move-target part
1823                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_part.id AND hold_type = 'P' LOOP
1824                 
1825                                 UPDATE  action.hold_request
1826                                   SET   target = target_part.id
1827                                   WHERE id = hold.id;
1828                 
1829                                 moved_objects := moved_objects + 1;
1830                         END LOOP;
1831
1832                 -- ... if not ...
1833                 ELSE
1834                         -- ... just move the part to the target record
1835                         UPDATE  biblio.monograph_part
1836                           SET   record = target_record
1837                           WHERE id = source_part.id;
1838                 END IF;
1839
1840                 moved_objects := moved_objects + 1;
1841         END LOOP;
1842
1843         -- Find multi_home items attached to the source ...
1844         FOR multi_home IN SELECT * FROM biblio.peer_bib_copy_map WHERE peer_record = source_record LOOP
1845                 -- ... and move them to the target record
1846                 UPDATE  biblio.peer_bib_copy_map
1847                   SET   peer_record = target_record
1848                   WHERE id = multi_home.id;
1849
1850                 moved_objects := moved_objects + 1;
1851         END LOOP;
1852
1853         -- And delete mappings where the item's home bib was merged with the peer bib
1854         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = (
1855                 SELECT (SELECT record FROM asset.call_number WHERE id = call_number)
1856                 FROM asset.copy WHERE id = target_copy
1857         );
1858
1859     -- Finally, "delete" the source record
1860     DELETE FROM biblio.record_entry WHERE id = source_record;
1861
1862         -- That's all, folks!
1863         RETURN moved_objects;
1864 END;
1865 $func$ LANGUAGE plpgsql;
1866
1867 -- from reporter-schema.sql
1868
1869 CREATE OR REPLACE VIEW reporter.simple_record AS
1870 SELECT  r.id,
1871         s.metarecord,
1872         r.fingerprint,
1873         r.quality,
1874         r.tcn_source,
1875         r.tcn_value,
1876         title.value AS title,
1877         uniform_title.value AS uniform_title,
1878         author.value AS author,
1879         publisher.value AS publisher,
1880         SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
1881         series_title.value AS series_title,
1882         series_statement.value AS series_statement,
1883         summary.value AS summary,
1884         ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
1885         ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
1886         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
1887         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
1888         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
1889         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
1890         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
1891         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
1892   FROM  biblio.record_entry r
1893         JOIN metabib.metarecord_source_map s ON (s.source = r.id)
1894         LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
1895         LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
1896         LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
1897         LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
1898         LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
1899         LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
1900         LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
1901         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')
1902         LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
1903         LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
1904   GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
1905
1906 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
1907 SELECT  r.id,
1908     r.fingerprint,
1909     r.quality,
1910     r.tcn_source,
1911     r.tcn_value,
1912     FIRST(title.value) AS title,
1913     FIRST(author.value) AS author,
1914     STRING_AGG(DISTINCT publisher.value, ', ') AS publisher,
1915     STRING_AGG(DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$), ', ') AS pubdate,
1916     CASE WHEN ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) = '{NULL}'
1917         THEN NULL
1918         ELSE ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') )
1919     END AS isbn,
1920     CASE WHEN ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) = '{NULL}'
1921         THEN NULL
1922         ELSE ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') )
1923     END AS issn
1924   FROM  biblio.record_entry r
1925     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
1926     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
1927     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')
1928     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')
1929     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
1930     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
1931   GROUP BY 1,2,3,4,5;
1932
1933
1934
1935 SELECT evergreen.upgrade_deps_block_check('0856', :eg_version);
1936
1937 CREATE OR REPLACE FUNCTION metabib.staged_browse(
1938     query                   TEXT,
1939     fields                  INT[],
1940     context_org             INT,
1941     context_locations       INT[],
1942     staff                   BOOL,
1943     browse_superpage_size   INT,
1944     count_up_from_zero      BOOL,   -- if false, count down from -1
1945     result_limit            INT,
1946     next_pivot_pos          INT
1947 ) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
1948 DECLARE
1949     curs                    REFCURSOR;
1950     rec                     RECORD;
1951     qpfts_query             TEXT;
1952     aqpfts_query            TEXT;
1953     afields                 INT[];
1954     bfields                 INT[];
1955     result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
1956     results_skipped         INT := 0;
1957     row_counter             INT := 0;
1958     row_number              INT;
1959     slice_start             INT;
1960     slice_end               INT;
1961     full_end                INT;
1962     all_records             BIGINT[];
1963     all_brecords             BIGINT[];
1964     all_arecords            BIGINT[];
1965     superpage_of_records    BIGINT[];
1966     superpage_size          INT;
1967 BEGIN
1968     IF count_up_from_zero THEN
1969         row_number := 0;
1970     ELSE
1971         row_number := -1;
1972     END IF;
1973
1974     OPEN curs FOR EXECUTE query;
1975
1976     LOOP
1977         FETCH curs INTO rec;
1978         IF NOT FOUND THEN
1979             IF result_row.pivot_point IS NOT NULL THEN
1980                 RETURN NEXT result_row;
1981             END IF;
1982             RETURN;
1983         END IF;
1984
1985
1986         -- Gather aggregate data based on the MBE row we're looking at now, authority axis
1987         SELECT INTO all_arecords, result_row.sees, afields
1988                 ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
1989                 STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
1990                 ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
1991
1992           FROM  metabib.browse_entry_simple_heading_map mbeshm
1993                 JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
1994                 JOIN authority.authority_linking aal ON ( ash.record = aal.source )
1995                 JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
1996                 JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
1997                     ash.atag = map.authority_field
1998                     AND map.metabib_field = ANY(fields)
1999                 )
2000           WHERE mbeshm.entry = rec.id;
2001
2002
2003         -- Gather aggregate data based on the MBE row we're looking at now, bib axis
2004         SELECT INTO all_brecords, result_row.authorities, bfields
2005                 ARRAY_AGG(DISTINCT source),
2006                 STRING_AGG(DISTINCT authority::TEXT, $$,$$),
2007                 ARRAY_AGG(DISTINCT def)
2008           FROM  metabib.browse_entry_def_map
2009           WHERE entry = rec.id
2010                 AND def = ANY(fields);
2011
2012         SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
2013
2014         result_row.sources := 0;
2015         result_row.asources := 0;
2016
2017         -- Bib-linked vis checking
2018         IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
2019
2020             full_end := ARRAY_LENGTH(all_brecords, 1);
2021             superpage_size := COALESCE(browse_superpage_size, full_end);
2022             slice_start := 1;
2023             slice_end := superpage_size;
2024
2025             WHILE result_row.sources = 0 AND slice_start <= full_end LOOP
2026                 superpage_of_records := all_brecords[slice_start:slice_end];
2027                 qpfts_query :=
2028                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
2029                     '1::INT AS rel FROM (SELECT UNNEST(' ||
2030                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
2031
2032                 -- We use search.query_parser_fts() for visibility testing.
2033                 -- We're calling it once per browse-superpage worth of records
2034                 -- out of the set of records related to a given mbe, until we've
2035                 -- either exhausted that set of records or found at least 1
2036                 -- visible record.
2037
2038                 SELECT INTO result_row.sources visible
2039                     FROM search.query_parser_fts(
2040                         context_org, NULL, qpfts_query, NULL,
2041                         context_locations, 0, NULL, NULL, FALSE, staff, FALSE
2042                     ) qpfts
2043                     WHERE qpfts.rel IS NULL;
2044
2045                 slice_start := slice_start + superpage_size;
2046                 slice_end := slice_end + superpage_size;
2047             END LOOP;
2048
2049             -- Accurate?  Well, probably.
2050             result_row.accurate := browse_superpage_size IS NULL OR
2051                 browse_superpage_size >= full_end;
2052
2053         END IF;
2054
2055         -- Authority-linked vis checking
2056         IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
2057
2058             full_end := ARRAY_LENGTH(all_arecords, 1);
2059             superpage_size := COALESCE(browse_superpage_size, full_end);
2060             slice_start := 1;
2061             slice_end := superpage_size;
2062
2063             WHILE result_row.asources = 0 AND slice_start <= full_end LOOP
2064                 superpage_of_records := all_arecords[slice_start:slice_end];
2065                 qpfts_query :=
2066                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
2067                     '1::INT AS rel FROM (SELECT UNNEST(' ||
2068                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
2069
2070                 -- We use search.query_parser_fts() for visibility testing.
2071                 -- We're calling it once per browse-superpage worth of records
2072                 -- out of the set of records related to a given mbe, via
2073                 -- authority until we've either exhausted that set of records
2074                 -- or found at least 1 visible record.
2075
2076                 SELECT INTO result_row.asources visible
2077                     FROM search.query_parser_fts(
2078                         context_org, NULL, qpfts_query, NULL,
2079                         context_locations, 0, NULL, NULL, FALSE, staff, FALSE
2080                     ) qpfts
2081                     WHERE qpfts.rel IS NULL;
2082
2083                 slice_start := slice_start + superpage_size;
2084                 slice_end := slice_end + superpage_size;
2085             END LOOP;
2086
2087
2088             -- Accurate?  Well, probably.
2089             result_row.aaccurate := browse_superpage_size IS NULL OR
2090                 browse_superpage_size >= full_end;
2091
2092         END IF;
2093
2094         IF result_row.sources > 0 OR result_row.asources > 0 THEN
2095
2096             -- The function that calls this function needs row_number in order
2097             -- to correctly order results from two different runs of this
2098             -- functions.
2099             result_row.row_number := row_number;
2100
2101             -- Now, if row_counter is still less than limit, return a row.  If
2102             -- not, but it is less than next_pivot_pos, continue on without
2103             -- returning actual result rows until we find
2104             -- that next pivot, and return it.
2105
2106             IF row_counter < result_limit THEN
2107                 result_row.browse_entry := rec.id;
2108                 result_row.value := rec.value;
2109
2110                 RETURN NEXT result_row;
2111             ELSE
2112                 result_row.browse_entry := NULL;
2113                 result_row.authorities := NULL;
2114                 result_row.fields := NULL;
2115                 result_row.value := NULL;
2116                 result_row.sources := NULL;
2117                 result_row.sees := NULL;
2118                 result_row.accurate := NULL;
2119                 result_row.aaccurate := NULL;
2120                 result_row.pivot_point := rec.id;
2121
2122                 IF row_counter >= next_pivot_pos THEN
2123                     RETURN NEXT result_row;
2124                     RETURN;
2125                 END IF;
2126             END IF;
2127
2128             IF count_up_from_zero THEN
2129                 row_number := row_number + 1;
2130             ELSE
2131                 row_number := row_number - 1;
2132             END IF;
2133
2134             -- row_counter is different from row_number.
2135             -- It simply counts up from zero so that we know when
2136             -- we've reached our limit.
2137             row_counter := row_counter + 1;
2138         END IF;
2139     END LOOP;
2140 END;
2141 $p$ LANGUAGE PLPGSQL;
2142
2143
2144 /*
2145  * Copyright (C) 2014  Equinox Software, Inc.
2146  * Mike Rylander <miker@esilibrary.com>
2147  *
2148  * This program is free software; you can redistribute it and/or
2149  * modify it under the terms of the GNU General Public License
2150  * as published by the Free Software Foundation; either version 2
2151  * of the License, or (at your option) any later version.
2152  *
2153  * This program is distributed in the hope that it will be useful,
2154  * but WITHOUT ANY WARRANTY; without even the implied warranty of
2155  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
2156  * GNU General Public License for more details.
2157  *
2158  */
2159
2160
2161
2162 SELECT evergreen.upgrade_deps_block_check('0857', :eg_version);
2163
2164 INSERT INTO config.global_flag (name, enabled, label)
2165 VALUES (
2166     'opac.located_uri.act_as_copy',
2167     FALSE,
2168     oils_i18n_gettext(
2169         'opac.located_uri.act_as_copy',
2170         'When enabled, Located URIs will provide visiblity behavior identical to copies.',
2171         'cgf',
2172         'label'
2173     )
2174 );
2175
2176 CREATE OR REPLACE FUNCTION search.query_parser_fts (
2177
2178     param_search_ou INT,
2179     param_depth     INT,
2180     param_query     TEXT,
2181     param_statuses  INT[],
2182     param_locations INT[],
2183     param_offset    INT,
2184     param_check     INT,
2185     param_limit     INT,
2186     metarecord      BOOL,
2187     staff           BOOL,
2188     deleted_search  BOOL,
2189     param_pref_ou   INT DEFAULT NULL
2190 ) RETURNS SETOF search.search_result AS $func$
2191 DECLARE
2192
2193     current_res         search.search_result%ROWTYPE;
2194     search_org_list     INT[];
2195     luri_org_list       INT[];
2196     tmp_int_list        INT[];
2197
2198     check_limit         INT;
2199     core_limit          INT;
2200     core_offset         INT;
2201     tmp_int             INT;
2202
2203     core_result         RECORD;
2204     core_cursor         REFCURSOR;
2205     core_rel_query      TEXT;
2206
2207     total_count         INT := 0;
2208     check_count         INT := 0;
2209     deleted_count       INT := 0;
2210     visible_count       INT := 0;
2211     excluded_count      INT := 0;
2212
2213     luri_as_copy        BOOL;
2214 BEGIN
2215
2216     check_limit := COALESCE( param_check, 1000 );
2217     core_limit  := COALESCE( param_limit, 25000 );
2218     core_offset := COALESCE( param_offset, 0 );
2219
2220     SELECT COALESCE( enabled, FALSE ) INTO luri_as_copy FROM config.global_flag WHERE name = 'opac.located_uri.act_as_copy';
2221
2222     -- core_skip_chk := COALESCE( param_skip_chk, 1 );
2223
2224     IF param_search_ou > 0 THEN
2225         IF param_depth IS NOT NULL THEN
2226             SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
2227         ELSE
2228             SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
2229         END IF;
2230
2231         IF luri_as_copy THEN
2232             SELECT ARRAY_AGG(distinct id) INTO luri_org_list FROM actor.org_unit_full_path( param_search_ou );
2233         ELSE
2234             SELECT ARRAY_AGG(distinct id) INTO luri_org_list FROM actor.org_unit_ancestors( param_search_ou );
2235         END IF;
2236
2237     ELSIF param_search_ou < 0 THEN
2238         SELECT ARRAY_AGG(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
2239
2240         FOR tmp_int IN SELECT * FROM UNNEST(search_org_list) LOOP
2241
2242             IF luri_as_copy THEN
2243                 SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_full_path( tmp_int );
2244             ELSE
2245                 SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( tmp_int );
2246             END IF;
2247
2248             luri_org_list := luri_org_list || tmp_int_list;
2249         END LOOP;
2250
2251         SELECT ARRAY_AGG(DISTINCT x.id) INTO luri_org_list FROM UNNEST(luri_org_list) x(id);
2252
2253     ELSIF param_search_ou = 0 THEN
2254         -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
2255     END IF;
2256
2257     IF param_pref_ou IS NOT NULL THEN
2258             IF luri_as_copy THEN
2259                 SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_full_path( param_pref_ou );
2260             ELSE
2261                 SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( param_pref_ou );
2262             END IF;
2263
2264         luri_org_list := luri_org_list || tmp_int_list;
2265     END IF;
2266
2267     OPEN core_cursor FOR EXECUTE param_query;
2268
2269     LOOP
2270
2271         FETCH core_cursor INTO core_result;
2272         EXIT WHEN NOT FOUND;
2273         EXIT WHEN total_count >= core_limit;
2274
2275         total_count := total_count + 1;
2276
2277         CONTINUE WHEN total_count NOT BETWEEN  core_offset + 1 AND check_limit + core_offset;
2278
2279         check_count := check_count + 1;
2280
2281         IF NOT deleted_search THEN
2282
2283             PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
2284             IF NOT FOUND THEN
2285                 -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
2286                 deleted_count := deleted_count + 1;
2287                 CONTINUE;
2288             END IF;
2289
2290             PERFORM 1
2291               FROM  biblio.record_entry b
2292                     JOIN config.bib_source s ON (b.source = s.id)
2293               WHERE s.transcendant
2294                     AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
2295
2296             IF FOUND THEN
2297                 -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
2298                 visible_count := visible_count + 1;
2299
2300                 current_res.id = core_result.id;
2301                 current_res.rel = core_result.rel;
2302
2303                 tmp_int := 1;
2304                 IF metarecord THEN
2305                     SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
2306                 END IF;
2307
2308                 IF tmp_int = 1 THEN
2309                     current_res.record = core_result.records[1];
2310                 ELSE
2311                     current_res.record = NULL;
2312                 END IF;
2313
2314                 RETURN NEXT current_res;
2315
2316                 CONTINUE;
2317             END IF;
2318
2319             PERFORM 1
2320               FROM  asset.call_number cn
2321                     JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
2322                     JOIN asset.uri uri ON (map.uri = uri.id)
2323               WHERE NOT cn.deleted
2324                     AND cn.label = '##URI##'
2325                     AND uri.active
2326                     AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
2327                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2328                     AND cn.owning_lib IN ( SELECT * FROM unnest( luri_org_list ) )
2329               LIMIT 1;
2330
2331             IF FOUND THEN
2332                 -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
2333                 visible_count := visible_count + 1;
2334
2335                 current_res.id = core_result.id;
2336                 current_res.rel = core_result.rel;
2337
2338                 tmp_int := 1;
2339                 IF metarecord THEN
2340                     SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
2341                 END IF;
2342
2343                 IF tmp_int = 1 THEN
2344                     current_res.record = core_result.records[1];
2345                 ELSE
2346                     current_res.record = NULL;
2347                 END IF;
2348
2349                 RETURN NEXT current_res;
2350
2351                 CONTINUE;
2352             END IF;
2353
2354             IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
2355
2356                 PERFORM 1
2357                   FROM  asset.call_number cn
2358                         JOIN asset.copy cp ON (cp.call_number = cn.id)
2359                   WHERE NOT cn.deleted
2360                         AND NOT cp.deleted
2361                         AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
2362                         AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2363                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2364                   LIMIT 1;
2365
2366                 IF NOT FOUND THEN
2367                     PERFORM 1
2368                       FROM  biblio.peer_bib_copy_map pr
2369                             JOIN asset.copy cp ON (cp.id = pr.target_copy)
2370                       WHERE NOT cp.deleted
2371                             AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
2372                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
2373                             AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2374                       LIMIT 1;
2375
2376                     IF NOT FOUND THEN
2377                     -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
2378                         excluded_count := excluded_count + 1;
2379                         CONTINUE;
2380                     END IF;
2381                 END IF;
2382
2383             END IF;
2384
2385             IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
2386
2387                 PERFORM 1
2388                   FROM  asset.call_number cn
2389                         JOIN asset.copy cp ON (cp.call_number = cn.id)
2390                   WHERE NOT cn.deleted
2391                         AND NOT cp.deleted
2392                         AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
2393                         AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2394                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2395                   LIMIT 1;
2396
2397                 IF NOT FOUND THEN
2398                     PERFORM 1
2399                       FROM  biblio.peer_bib_copy_map pr
2400                             JOIN asset.copy cp ON (cp.id = pr.target_copy)
2401                       WHERE NOT cp.deleted
2402                             AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
2403                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
2404                             AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2405                       LIMIT 1;
2406
2407                     IF NOT FOUND THEN
2408                         -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
2409                         excluded_count := excluded_count + 1;
2410                         CONTINUE;
2411                     END IF;
2412                 END IF;
2413
2414             END IF;
2415
2416             IF staff IS NULL OR NOT staff THEN
2417
2418                 PERFORM 1
2419                   FROM  asset.opac_visible_copies
2420                   WHERE circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2421                         AND record IN ( SELECT * FROM unnest( core_result.records ) )
2422                   LIMIT 1;
2423
2424                 IF NOT FOUND THEN
2425                     PERFORM 1
2426                       FROM  biblio.peer_bib_copy_map pr
2427                             JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
2428                       WHERE cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2429                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
2430                       LIMIT 1;
2431
2432                     IF NOT FOUND THEN
2433
2434                         -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
2435                         excluded_count := excluded_count + 1;
2436                         CONTINUE;
2437                     END IF;
2438                 END IF;
2439
2440             ELSE
2441
2442                 PERFORM 1
2443                   FROM  asset.call_number cn
2444                         JOIN asset.copy cp ON (cp.call_number = cn.id)
2445                   WHERE NOT cn.deleted
2446                         AND NOT cp.deleted
2447                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2448                         AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2449                   LIMIT 1;
2450
2451                 IF NOT FOUND THEN
2452
2453                     PERFORM 1
2454                       FROM  biblio.peer_bib_copy_map pr
2455                             JOIN asset.copy cp ON (cp.id = pr.target_copy)
2456                       WHERE NOT cp.deleted
2457                             AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
2458                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
2459                       LIMIT 1;
2460
2461                     IF NOT FOUND THEN
2462
2463                         PERFORM 1
2464                           FROM  asset.call_number cn
2465                                 JOIN asset.copy cp ON (cp.call_number = cn.id)
2466                           WHERE cn.record IN ( SELECT * FROM unnest( core_result.records ) )
2467                                 AND NOT cp.deleted
2468                           LIMIT 1;
2469
2470                         IF FOUND THEN
2471                             -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
2472                             excluded_count := excluded_count + 1;
2473                             CONTINUE;
2474                         END IF;
2475                     END IF;
2476
2477                 END IF;
2478
2479             END IF;
2480
2481         END IF;
2482
2483         visible_count := visible_count + 1;
2484
2485         current_res.id = core_result.id;
2486         current_res.rel = core_result.rel;
2487
2488         tmp_int := 1;
2489         IF metarecord THEN
2490             SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
2491         END IF;
2492
2493         IF tmp_int = 1 THEN
2494             current_res.record = core_result.records[1];
2495         ELSE
2496             current_res.record = NULL;
2497         END IF;
2498
2499         RETURN NEXT current_res;
2500
2501         IF visible_count % 1000 = 0 THEN
2502             -- RAISE NOTICE ' % visible so far ... ', visible_count;
2503         END IF;
2504
2505     END LOOP;
2506
2507     current_res.id = NULL;
2508     current_res.rel = NULL;
2509     current_res.record = NULL;
2510     current_res.total = total_count;
2511     current_res.checked = check_count;
2512     current_res.deleted = deleted_count;
2513     current_res.visible = visible_count;
2514     current_res.excluded = excluded_count;
2515
2516     CLOSE core_cursor;
2517
2518     RETURN NEXT current_res;
2519
2520 END;
2521 $func$ LANGUAGE PLPGSQL;
2522
2523 CREATE OR REPLACE FUNCTION unapi.holdings_xml (
2524     bid BIGINT,
2525     ouid INT,
2526     org TEXT,
2527     depth INT DEFAULT NULL,
2528     includes TEXT[] DEFAULT NULL::TEXT[],
2529     slimit HSTORE DEFAULT NULL,
2530     soffset HSTORE DEFAULT NULL,
2531     include_xmlns BOOL DEFAULT TRUE,
2532     pref_lib INT DEFAULT NULL
2533 )
2534 RETURNS XML AS $F$
2535      SELECT  XMLELEMENT(
2536                  name holdings,
2537                  XMLATTRIBUTES(
2538                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2539                     CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id,
2540                     (SELECT record_has_holdable_copy FROM asset.record_has_holdable_copy($1)) AS has_holdable
2541                  ),
2542                  XMLELEMENT(
2543                      name counts,
2544                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
2545                          SELECT  XMLELEMENT(
2546                                      name count,
2547                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2548                                  )::text
2549                            FROM  asset.opac_ou_record_copy_count($2,  $1)
2550                                      UNION
2551                          SELECT  XMLELEMENT(
2552                                      name count,
2553                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2554                                  )::text
2555                            FROM  asset.staff_ou_record_copy_count($2, $1)
2556                                      UNION
2557                          SELECT  XMLELEMENT(
2558                                      name count,
2559                                      XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2560                                  )::text
2561                            FROM  asset.opac_ou_record_copy_count($9,  $1)
2562                                      ORDER BY 1
2563                      )x)
2564                  ),
2565                  CASE
2566                      WHEN ('bmp' = ANY ($5)) THEN
2567                         XMLELEMENT(
2568                             name monograph_parts,
2569                             (SELECT XMLAGG(bmp) FROM (
2570                                 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)
2571                                   FROM  biblio.monograph_part
2572                                   WHERE record = $1
2573                             )x)
2574                         )
2575                      ELSE NULL
2576                  END,
2577                  XMLELEMENT(
2578                      name volumes,
2579                      (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
2580                         -- Physical copies
2581                         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
2582                         FROM evergreen.ranked_volumes($1, $2, $4, $6, $7, $9, $5) AS y
2583                         UNION ALL
2584                         -- Located URIs
2585                         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
2586                         FROM evergreen.located_uris($1, $2, $9) AS uris
2587                      )x)
2588                  ),
2589                  CASE WHEN ('ssub' = ANY ($5)) THEN
2590                      XMLELEMENT(
2591                          name subscriptions,
2592                          (SELECT XMLAGG(ssub) FROM (
2593                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
2594                               FROM  serial.subscription
2595                               WHERE record_entry = $1
2596                         )x)
2597                      )
2598                  ELSE NULL END,
2599                  CASE WHEN ('acp' = ANY ($5)) THEN
2600                      XMLELEMENT(
2601                          name foreign_copies,
2602                          (SELECT XMLAGG(acp) FROM (
2603                             SELECT  unapi.acp(p.target_copy,'xml','copy',evergreen.array_remove_item_by_value($5,'acp'), $3, $4, $6, $7, FALSE)
2604                               FROM  biblio.peer_bib_copy_map p
2605                                     JOIN asset.copy c ON (p.target_copy = c.id)
2606                               WHERE NOT c.deleted AND p.peer_record = $1
2607                             LIMIT ($6 -> 'acp')::INT
2608                             OFFSET ($7 -> 'acp')::INT
2609                         )x)
2610                      )
2611                  ELSE NULL END
2612              );
2613 $F$ LANGUAGE SQL STABLE;
2614
2615
2616
2617 SELECT evergreen.upgrade_deps_block_check('0858', :eg_version);
2618
2619 -- Fix faulty seed data. Otherwise for ptype 'f' we have subfield 'e'
2620 -- overlapping subfield 'd'
2621 UPDATE config.marc21_physical_characteristic_subfield_map
2622     SET start_pos = 5
2623     WHERE ptype_key = 'f' AND subfield = 'e';
2624
2625 -- Evergreen DB patch 0859.data.staff-initials-settings.sql
2626 --
2627 -- More granular configuration settings for requiring use of staff initials
2628 --
2629
2630 -- check whether patch can be applied
2631 SELECT evergreen.upgrade_deps_block_check('0859', :eg_version);
2632
2633 -- add new granular settings for requiring use of staff initials
2634 INSERT INTO config.org_unit_setting_type
2635     (name, grp, label, description, datatype)
2636     VALUES (
2637         'ui.staff.require_initials.patron_standing_penalty',
2638         'gui',
2639         oils_i18n_gettext(
2640             'ui.staff.require_initials.patron_standing_penalty',
2641             'Require staff initials for entry/edit of patron standing penalties and messages.',
2642             'coust',
2643             'label'
2644         ),
2645         oils_i18n_gettext(
2646             'ui.staff.require_initials.patron_standing_penalty',
2647             'Appends staff initials and edit date into patron standing penalties and messages.',
2648             'coust',
2649             'description'
2650         ),
2651         'bool'
2652     ), (
2653         'ui.staff.require_initials.patron_info_notes',
2654         'gui',
2655         oils_i18n_gettext(
2656             'ui.staff.require_initials.patron_info_notes',
2657             'Require staff initials for entry/edit of patron notes.',
2658             'coust',
2659             'label'
2660         ),
2661         oils_i18n_gettext(
2662             'ui.staff.require_initials.patron_info_notes',
2663             'Appends staff initials and edit date into patron note content.',
2664             'coust',
2665             'description'
2666         ),
2667         'bool'
2668     ), (
2669         'ui.staff.require_initials.copy_notes',
2670         'gui',
2671         oils_i18n_gettext(
2672             'ui.staff.require_initials.copy_notes',
2673             'Require staff initials for entry/edit of copy notes.',
2674             'coust',
2675             'label'
2676         ),
2677         oils_i18n_gettext(
2678             'ui.staff.require_initials.copy_notes',
2679             'Appends staff initials and edit date into copy note content..',
2680             'coust',
2681             'description'
2682         ),
2683         'bool'
2684     );
2685
2686 -- Update any existing setting so that the original set value is now passed to
2687 -- one of the newer settings.
2688
2689 UPDATE actor.org_unit_setting
2690 SET name = 'ui.staff.require_initials.patron_standing_penalty'
2691 WHERE name = 'ui.staff.require_initials';
2692
2693 -- Add similar values for new settings as old ones to preserve existing configured
2694 -- functionality.
2695
2696 INSERT INTO actor.org_unit_setting (org_unit, name, value)
2697 SELECT org_unit, 'ui.staff.require_initials.patron_info_notes', value
2698 FROM actor.org_unit_setting
2699 WHERE name = 'ui.staff.require_initials.patron_standing_penalty';
2700
2701 INSERT INTO actor.org_unit_setting (org_unit, name, value)
2702 SELECT org_unit, 'ui.staff.require_initials.copy_notes', value
2703 FROM actor.org_unit_setting
2704 WHERE name = 'ui.staff.require_initials.patron_standing_penalty';
2705
2706 -- Update setting logs so that the original setting name's history is now transferred
2707 -- over to one of the newer settings.
2708
2709 UPDATE config.org_unit_setting_type_log
2710 SET field_name = 'ui.staff.require_initials.patron_standing_penalty'
2711 WHERE field_name = 'ui.staff.require_initials';
2712
2713 -- Remove the old setting entirely
2714
2715 DELETE FROM config.org_unit_setting_type WHERE name = 'ui.staff.require_initials';
2716
2717
2718 -- oh, the irony
2719 SELECT evergreen.upgrade_deps_block_check('0860', :eg_version);
2720
2721 CREATE OR REPLACE FUNCTION evergreen.array_overlap_check (/* field */) RETURNS TRIGGER AS $$
2722 DECLARE
2723     fld     TEXT;
2724     cnt     INT;
2725 BEGIN
2726     fld := TG_ARGV[0];
2727     EXECUTE 'SELECT COUNT(*) FROM '|| TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME ||' WHERE '|| fld ||' && ($1).'|| fld INTO cnt USING NEW;
2728     IF cnt > 0 THEN
2729         RAISE EXCEPTION 'Cannot insert duplicate array into field % of table %', fld, TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME;
2730     END IF;
2731     RETURN NEW;
2732 END;
2733 $$ LANGUAGE PLPGSQL;
2734
2735
2736 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecates ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
2737     SELECT  DISTINCT l.version
2738       FROM  config.upgrade_log l
2739             JOIN config.db_patch_dependencies d ON (l.version = ANY(d.deprecates))
2740       WHERE d.db_patch = $1
2741 $$ LANGUAGE SQL;
2742
2743 -- List applied db patches that are superseded by (and block the application of) my_db_patch
2744 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_supersedes ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
2745     SELECT  DISTINCT l.version
2746       FROM  config.upgrade_log l
2747             JOIN config.db_patch_dependencies d ON (l.version = ANY(d.supersedes))
2748       WHERE d.db_patch = $1
2749 $$ LANGUAGE SQL;
2750
2751
2752 CREATE OR REPLACE FUNCTION evergreen.upgrade_deps_block_check ( my_db_patch TEXT, my_applied_to TEXT ) RETURNS BOOL AS $$
2753 DECLARE 
2754     deprecates TEXT;
2755     supersedes TEXT;
2756 BEGIN
2757     IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
2758         SELECT  STRING_AGG(patch, ', ') INTO deprecates FROM evergreen.upgrade_list_applied_deprecates(my_db_patch);
2759         SELECT  STRING_AGG(patch, ', ') INTO supersedes FROM evergreen.upgrade_list_applied_supersedes(my_db_patch);
2760         RAISE EXCEPTION '
2761 Upgrade script % can not be applied:
2762   applied deprecated scripts %
2763   applied superseded scripts %
2764   deprecated by %
2765   superseded by %',
2766             my_db_patch,
2767             (SELECT ARRAY_AGG(patch) FROM evergreen.upgrade_list_applied_deprecates(my_db_patch)),
2768             (SELECT ARRAY_AGG(patch) FROM evergreen.upgrade_list_applied_supersedes(my_db_patch)),
2769             evergreen.upgrade_list_applied_deprecated(my_db_patch),
2770             evergreen.upgrade_list_applied_superseded(my_db_patch);
2771     END IF;
2772
2773     INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
2774     RETURN TRUE;
2775 END;
2776 $$ LANGUAGE PLPGSQL;
2777
2778
2779
2780 SELECT evergreen.upgrade_deps_block_check('0861', :eg_version);
2781
2782 CREATE INDEX authority_record_entry_create_date_idx ON authority.record_entry ( create_date );
2783 CREATE INDEX authority_record_entry_edit_date_idx ON authority.record_entry ( edit_date );
2784
2785
2786
2787 SELECT evergreen.upgrade_deps_block_check('0863', :eg_version);
2788
2789
2790 -- cheat sheet for enabling Stripe payments:
2791 --  'credit.payments.allow' must be true, and among other things it drives the
2792 --      opac to render a payment form at all
2793 --  NEW 'credit.processor.stripe.enabled' must be true  (kind of redundant but
2794 --      my fault for setting the precedent with c.p.{authorizenet|paypal|payflowpro}.enabled)
2795 --  'credit.default.processor' must be 'Stripe'
2796 --  NEW 'credit.processor.stripe.pubkey' must be set
2797 --  NEW 'credit.processor.stripe.secretkey' must be set
2798
2799 INSERT into config.org_unit_setting_type
2800 ( name, grp, label, description, datatype, fm_class ) VALUES
2801
2802     ( 'credit.processor.stripe.enabled', 'credit',
2803     oils_i18n_gettext('credit.processor.stripe.enabled',
2804         'Enable Stripe payments',
2805         'coust', 'label'),
2806     oils_i18n_gettext('credit.processor.stripe.enabled',
2807         'Enable Stripe payments',
2808         'coust', 'description'),
2809     'bool', null)
2810
2811 ,( 'credit.processor.stripe.pubkey', 'credit',
2812     oils_i18n_gettext('credit.processor.stripe.pubkey',
2813         'Stripe publishable key',
2814         'coust', 'label'),
2815     oils_i18n_gettext('credit.processor.stripe.pubkey',
2816         'Stripe publishable key',
2817         'coust', 'description'),
2818     'string', null)
2819
2820 ,( 'credit.processor.stripe.secretkey', 'credit',
2821     oils_i18n_gettext('credit.processor.stripe.secretkey',
2822         'Stripe secret key',
2823         'coust', 'label'),
2824     oils_i18n_gettext('credit.processor.stripe.secretkey',
2825         'Stripe secret key',
2826         'coust', 'description'),
2827     'string', null)
2828 ;
2829
2830 UPDATE config.org_unit_setting_type
2831 SET description = 'This might be "AuthorizeNet", "PayPal", "PayflowPro", or "Stripe".'
2832 WHERE name = 'credit.processor.default' AND description = 'This might be "AuthorizeNet", "PayPal", etc.'; -- don't clobber local edits or i18n
2833
2834
2835 SELECT evergreen.upgrade_deps_block_check('0864', :eg_version);
2836
2837 CREATE EXTENSION intarray;
2838
2839 -- while we have this opportunity, and before we start collecting 
2840 -- CCVM IDs (below) carve out a nice space for stock ccvm values
2841 UPDATE config.coded_value_map SET id = id + 10000 WHERE id > 556;
2842 SELECT SETVAL('config.coded_value_map_id_seq'::TEXT, 
2843     (SELECT GREATEST(max(id), 10000) FROM config.coded_value_map));
2844
2845 ALTER TABLE config.record_attr_definition ADD COLUMN multi BOOL NOT NULL DEFAULT TRUE, ADD COLUMN composite BOOL NOT NULL DEFAULT FALSE;
2846
2847 UPDATE  config.record_attr_definition
2848   SET   multi = FALSE
2849   WHERE name IN ('bib_level','control_type','pubdate','cat_form','enc_level','item_type','titlesort','authorsort');
2850
2851 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
2852 DECLARE
2853     rowid   INT := 0;
2854     _007    TEXT;
2855     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
2856     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
2857     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
2858     retval  biblio.marc21_physical_characteristics%ROWTYPE;
2859 BEGIN
2860
2861     FOR _007 IN SELECT oils_xpath_string('//*', value) FROM UNNEST(oils_xpath('//*[@tag="007"]', marc)) x(value) LOOP
2862         IF _007 IS NOT NULL AND _007 <> '' THEN
2863             SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
2864
2865             IF ptype.ptype_key IS NOT NULL THEN
2866                 FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
2867                     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 );
2868
2869                     IF pval.id IS NOT NULL THEN
2870                         rowid := rowid + 1;
2871                         retval.id := rowid;
2872                         retval.ptype := ptype.ptype_key;
2873                         retval.subfield := psf.id;
2874                         retval.value := pval.id;
2875                         RETURN NEXT retval;
2876                     END IF;
2877
2878                 END LOOP;
2879             END IF;
2880         END IF;
2881     END LOOP;
2882
2883     RETURN;
2884 END;
2885 $func$ LANGUAGE PLPGSQL;
2886
2887 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field_list( marc TEXT, ff TEXT ) RETURNS TEXT[] AS $func$
2888 DECLARE
2889     rtype       TEXT;
2890     ff_pos      RECORD;
2891     tag_data    RECORD;
2892     val         TEXT;
2893     collection  TEXT[] := '{}'::TEXT[];
2894 BEGIN
2895     rtype := (vandelay.marc21_record_type( marc )).code;
2896     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
2897         IF ff_pos.tag = 'ldr' THEN
2898             val := oils_xpath_string('//*[local-name()="leader"]', marc);
2899             IF val IS NOT NULL THEN
2900                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
2901                 collection := collection || val;
2902             END IF;
2903         ELSE
2904             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP