Forward-port 2.5.2-2.5.3 upgrade script
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.5.2-2.5.3-upgrade-db.sql
1 --Upgrade Script for 2.5.2 to 2.5.3
2 \set eg_version '''2.5.3'''
3
4 \qecho *** This ALTER TABLE might fail depending on your DB vintage. ***
5 \qecho *** It should be harmless. ***
6 ALTER TABLE action.aged_hold_request ADD COLUMN behind_desk BOOLEAN;
7
8 BEGIN;
9 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('2.5.3', :eg_version);
10
11 -- this file is a duplicate of 0851, moved up for better backport clarity
12
13 -- check whether patch can be applied
14 SELECT evergreen.upgrade_deps_block_check('0862', :eg_version);
15
16 CREATE OR REPLACE FUNCTION evergreen.maintain_901 () RETURNS TRIGGER AS $func$
17 use strict;
18 use MARC::Record;
19 use MARC::File::XML (BinaryEncoding => 'UTF-8');
20 use MARC::Charset;
21 use Encode;
22 use Unicode::Normalize;
23
24 MARC::Charset->assume_unicode(1);
25
26 my $schema = $_TD->{table_schema};
27 my $marc = MARC::Record->new_from_xml($_TD->{new}{marc});
28
29 my @old901s = $marc->field('901');
30 $marc->delete_fields(@old901s);
31
32 if ($schema eq 'biblio') {
33     my $tcn_value = $_TD->{new}{tcn_value};
34
35     # Set TCN value to record ID?
36     my $id_as_tcn = spi_exec_query("
37         SELECT enabled
38         FROM config.global_flag
39         WHERE name = 'cat.bib.use_id_for_tcn'
40     ");
41     if (($id_as_tcn->{processed}) && $id_as_tcn->{rows}[0]->{enabled} eq 't') {
42         $tcn_value = $_TD->{new}{id}; 
43         $_TD->{new}{tcn_value} = $tcn_value;
44     }
45
46     my $new_901 = MARC::Field->new("901", " ", " ",
47         "a" => $tcn_value,
48         "b" => $_TD->{new}{tcn_source},
49         "c" => $_TD->{new}{id},
50         "t" => $schema
51     );
52
53     if ($_TD->{new}{owner}) {
54         $new_901->add_subfields("o" => $_TD->{new}{owner});
55     }
56
57     if ($_TD->{new}{share_depth}) {
58         $new_901->add_subfields("d" => $_TD->{new}{share_depth});
59     }
60
61     $marc->append_fields($new_901);
62 } elsif ($schema eq 'authority') {
63     my $new_901 = MARC::Field->new("901", " ", " ",
64         "c" => $_TD->{new}{id},
65         "t" => $schema,
66     );
67     $marc->append_fields($new_901);
68 } elsif ($schema eq 'serial') {
69     my $new_901 = MARC::Field->new("901", " ", " ",
70         "c" => $_TD->{new}{id},
71         "t" => $schema,
72         "o" => $_TD->{new}{owning_lib},
73     );
74
75     if ($_TD->{new}{record}) {
76         $new_901->add_subfields("r" => $_TD->{new}{record});
77     }
78
79     $marc->append_fields($new_901);
80 } else {
81     my $new_901 = MARC::Field->new("901", " ", " ",
82         "c" => $_TD->{new}{id},
83         "t" => $schema,
84     );
85     $marc->append_fields($new_901);
86 }
87
88 my $xml = $marc->as_xml_record();
89 $xml =~ s/\n//sgo;
90 $xml =~ s/^<\?xml.+\?\s*>//go;
91 $xml =~ s/>\s+</></go;
92 $xml =~ s/\p{Cc}//go;
93
94 # Embed a version of OpenILS::Application::AppUtils->entityize()
95 # to avoid having to set PERL5LIB for PostgreSQL as well
96
97 $xml = NFC($xml);
98
99 # Convert raw ampersands to entities
100 $xml =~ s/&(?!\S+;)/&amp;/gso;
101
102 # Convert Unicode characters to entities
103 $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
104
105 $xml =~ s/[\x00-\x1f]//go;
106 $_TD->{new}{marc} = $xml;
107
108 return "MODIFY";
109 $func$ LANGUAGE PLPERLU;
110
111 CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
112 use strict;
113 use MARC::Record;
114 use MARC::File::XML (BinaryEncoding => 'UTF-8');
115 use MARC::Charset;
116 use Encode;
117 use Unicode::Normalize;
118
119 MARC::Charset->assume_unicode(1);
120
121 my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
122 my $schema = $_TD->{table_schema};
123 my $rec_id = $_TD->{new}{id};
124
125 # Short-circuit if maintaining control numbers per MARC21 spec is not enabled
126 my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
127 if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
128     return;
129 }
130
131 # Get the control number identifier from an OU setting based on $_TD->{new}{owner}
132 my $ou_cni = 'EVRGRN';
133
134 my $owner;
135 if ($schema eq 'serial') {
136     $owner = $_TD->{new}{owning_lib};
137 } else {
138     # are.owner and bre.owner can be null, so fall back to the consortial setting
139     $owner = $_TD->{new}{owner} || 1;
140 }
141
142 my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
143 if ($ous_rv->{processed}) {
144     $ou_cni = $ous_rv->{rows}[0]->{value};
145     $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
146 } else {
147     # Fall back to the shortname of the OU if there was no OU setting
148     $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
149     if ($ous_rv->{processed}) {
150         $ou_cni = $ous_rv->{rows}[0]->{shortname};
151     }
152 }
153
154 my ($create, $munge) = (0, 0);
155
156 my @scns = $record->field('035');
157
158 foreach my $id_field ('001', '003') {
159     my $spec_value;
160     my @controls = $record->field($id_field);
161
162     if ($id_field eq '001') {
163         $spec_value = $rec_id;
164     } else {
165         $spec_value = $ou_cni;
166     }
167
168     # Create the 001/003 if none exist
169     if (scalar(@controls) == 1) {
170         # Only one field; check to see if we need to munge it
171         unless (grep $_->data() eq $spec_value, @controls) {
172             $munge = 1;
173         }
174     } else {
175         # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
176         foreach my $control (@controls) {
177             $record->delete_field($control);
178         }
179         $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
180         $create = 1;
181     }
182 }
183
184 my $cn = $record->field('001')->data();
185 # Special handling of OCLC numbers, often found in records that lack 003
186 if ($cn =~ /^o(c[nm]|n)\d/) {
187     $cn =~ s/^o(c[nm]|n)0*(\d+)/$2/;
188     $record->field('003')->data('OCoLC');
189     $create = 0;
190 }
191
192 # Now, if we need to munge the 001, we will first push the existing 001/003
193 # into the 035; but if the record did not have one (and one only) 001 and 003
194 # to begin with, skip this process
195 if ($munge and not $create) {
196
197     my $scn = "(" . $record->field('003')->data() . ")" . $cn;
198
199     # Do not create duplicate 035 fields
200     unless (grep $_->subfield('a') eq $scn, @scns) {
201         $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
202     }
203 }
204
205 # Set the 001/003 and update the MARC
206 if ($create or $munge) {
207     $record->field('001')->data($rec_id);
208     $record->field('003')->data($ou_cni);
209
210     my $xml = $record->as_xml_record();
211     $xml =~ s/\n//sgo;
212     $xml =~ s/^<\?xml.+\?\s*>//go;
213     $xml =~ s/>\s+</></go;
214     $xml =~ s/\p{Cc}//go;
215
216     # Embed a version of OpenILS::Application::AppUtils->entityize()
217     # to avoid having to set PERL5LIB for PostgreSQL as well
218
219     $xml = NFC($xml);
220
221     # Convert raw ampersands to entities
222     $xml =~ s/&(?!\S+;)/&amp;/gso;
223
224     # Convert Unicode characters to entities
225     $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
226
227     $xml =~ s/[\x00-\x1f]//go;
228     $_TD->{new}{marc} = $xml;
229
230     return "MODIFY";
231 }
232
233 return;
234 $func$ LANGUAGE PLPERLU;
235
236 CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
237
238     use strict;
239     use Unicode::Normalize;
240     use Encode;
241
242     my $str = shift;
243     my $sf = shift;
244
245     # Apply NACO normalization to input string; based on
246     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
247     #
248     # Note that unlike a strict reading of the NACO normalization rules,
249     # output is returned as lowercase instead of uppercase for compatibility
250     # with previous versions of the Evergreen naco_normalize routine.
251
252     # Convert to upper-case first; even though final output will be lowercase, doing this will
253     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
254     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
255     $str = uc $str;
256
257     # remove non-filing strings
258     $str =~ s/\x{0098}.*?\x{009C}//g;
259
260     $str = NFKD($str);
261
262     # additional substitutions - 3.6.
263     $str =~ s/\x{00C6}/AE/g;
264     $str =~ s/\x{00DE}/TH/g;
265     $str =~ s/\x{0152}/OE/g;
266     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
267
268     # transformations based on Unicode category codes
269     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
270
271         if ($sf && $sf =~ /^a/o) {
272                 my $commapos = index($str, ',');
273                 if ($commapos > -1) {
274                         if ($commapos != length($str) - 1) {
275                 $str =~ s/,/\x07/; # preserve first comma
276                         }
277                 }
278         }
279
280     # since we've stripped out the control characters, we can now
281     # use a few as placeholders temporarily
282     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
283     $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;
284     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
285
286     # decimal digits
287     $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/;
288
289     # intentionally skipping step 8 of the NACO algorithm; if the string
290     # gets normalized away, that's fine.
291
292     # leading and trailing spaces
293     $str =~ s/\s+/ /g;
294     $str =~ s/^\s+//;
295     $str =~ s/\s+$//g;
296
297     return lc $str;
298 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
299
300 -- Currently, the only difference from naco_normalize is that search_normalize
301 -- turns apostrophes into spaces, while naco_normalize collapses them.
302 CREATE OR REPLACE FUNCTION public.search_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
303
304     use strict;
305     use Unicode::Normalize;
306     use Encode;
307
308     my $str = shift;
309     my $sf = shift;
310
311     # Apply NACO normalization to input string; based on
312     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
313     #
314     # Note that unlike a strict reading of the NACO normalization rules,
315     # output is returned as lowercase instead of uppercase for compatibility
316     # with previous versions of the Evergreen naco_normalize routine.
317
318     # Convert to upper-case first; even though final output will be lowercase, doing this will
319     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
320     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
321     $str = uc $str;
322
323     # remove non-filing strings
324     $str =~ s/\x{0098}.*?\x{009C}//g;
325
326     $str = NFKD($str);
327
328     # additional substitutions - 3.6.
329     $str =~ s/\x{00C6}/AE/g;
330     $str =~ s/\x{00DE}/TH/g;
331     $str =~ s/\x{0152}/OE/g;
332     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}][/DDOLl/d;
333
334     # transformations based on Unicode category codes
335     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
336
337         if ($sf && $sf =~ /^a/o) {
338                 my $commapos = index($str, ',');
339                 if ($commapos > -1) {
340                         if ($commapos != length($str) - 1) {
341                 $str =~ s/,/\x07/; # preserve first comma
342                         }
343                 }
344         }
345
346     # since we've stripped out the control characters, we can now
347     # use a few as placeholders temporarily
348     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
349     $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;
350     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
351
352     # decimal digits
353     $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/;
354
355     # intentionally skipping step 8 of the NACO algorithm; if the string
356     # gets normalized away, that's fine.
357
358     # leading and trailing spaces
359     $str =~ s/\s+/ /g;
360     $str =~ s/^\s+//;
361     $str =~ s/\s+$//g;
362
363     return lc $str;
364 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
365
366 -- add missing behind_desk column
367
368
369 SELECT evergreen.upgrade_deps_block_check('0868', :eg_version);
370
371 CREATE OR REPLACE VIEW action.all_hold_request AS
372     SELECT DISTINCT
373            COALESCE(a.post_code, b.post_code) AS usr_post_code,
374            p.home_ou AS usr_home_ou,
375            p.profile AS usr_profile,
376            EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
377            CAST(ahr.requestor <> ahr.usr AS BOOLEAN) AS staff_placed,
378            ahr.id,
379            ahr.request_time,
380            ahr.capture_time,
381            ahr.fulfillment_time,
382            ahr.checkin_time,
383            ahr.return_time,
384            ahr.prev_check_time,
385            ahr.expire_time,
386            ahr.cancel_time,
387            ahr.cancel_cause,
388            ahr.cancel_note,
389            ahr.target,
390            ahr.current_copy,
391            ahr.fulfillment_staff,
392            ahr.fulfillment_lib,
393            ahr.request_lib,
394            ahr.selection_ou,
395            ahr.selection_depth,
396            ahr.pickup_lib,
397            ahr.hold_type,
398            ahr.holdable_formats,
399            CASE
400            WHEN ahr.phone_notify IS NULL THEN FALSE
401            WHEN ahr.phone_notify = '' THEN FALSE
402            ELSE TRUE
403            END AS phone_notify,
404            ahr.email_notify,
405            CASE
406            WHEN ahr.sms_notify IS NULL THEN FALSE
407            WHEN ahr.sms_notify = '' THEN FALSE
408            ELSE TRUE
409            END AS sms_notify,
410            ahr.frozen,
411            ahr.thaw_date,
412            ahr.shelf_time,
413            ahr.cut_in_line,
414            ahr.mint_condition,
415            ahr.shelf_expire_time,
416            ahr.current_shelf_lib,
417            ahr.behind_desk
418     FROM action.hold_request ahr
419          JOIN actor.usr p ON (ahr.usr = p.id)
420          LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
421          LEFT JOIN actor.usr_address b ON (p.billing_address = b.id)
422     UNION ALL
423     SELECT 
424            usr_post_code,
425            usr_home_ou,
426            usr_profile,
427            usr_birth_year,
428            staff_placed,
429            id,
430            request_time,
431            capture_time,
432            fulfillment_time,
433            checkin_time,
434            return_time,
435            prev_check_time,
436            expire_time,
437            cancel_time,
438            cancel_cause,
439            cancel_note,
440            target,
441            current_copy,
442            fulfillment_staff,
443            fulfillment_lib,
444            request_lib,
445            selection_ou,
446            selection_depth,
447            pickup_lib,
448            hold_type,
449            holdable_formats,
450            phone_notify,
451            email_notify,
452            sms_notify,
453            frozen,
454            thaw_date,
455            shelf_time,
456            cut_in_line,
457            mint_condition,
458            shelf_expire_time,
459            current_shelf_lib,
460            behind_desk
461     FROM action.aged_hold_request;
462
463
464
465 CREATE OR REPLACE FUNCTION action.age_hold_on_delete () RETURNS TRIGGER AS $$
466 DECLARE
467 BEGIN
468     -- Archive a copy of the old row to action.aged_hold_request
469
470     INSERT INTO action.aged_hold_request
471            (usr_post_code,
472             usr_home_ou,
473             usr_profile,
474             usr_birth_year,
475             staff_placed,
476             id,
477             request_time,
478             capture_time,
479             fulfillment_time,
480             checkin_time,
481             return_time,
482             prev_check_time,
483             expire_time,
484             cancel_time,
485             cancel_cause,
486             cancel_note,
487             target,
488             current_copy,
489             fulfillment_staff,
490             fulfillment_lib,
491             request_lib,
492             selection_ou,
493             selection_depth,
494             pickup_lib,
495             hold_type,
496             holdable_formats,
497             phone_notify,
498             email_notify,
499             sms_notify,
500             frozen,
501             thaw_date,
502             shelf_time,
503             cut_in_line,
504             mint_condition,
505             shelf_expire_time,
506             current_shelf_lib,
507             behind_desk)
508       SELECT 
509            usr_post_code,
510            usr_home_ou,
511            usr_profile,
512            usr_birth_year,
513            staff_placed,
514            id,
515            request_time,
516            capture_time,
517            fulfillment_time,
518            checkin_time,
519            return_time,
520            prev_check_time,
521            expire_time,
522            cancel_time,
523            cancel_cause,
524            cancel_note,
525            target,
526            current_copy,
527            fulfillment_staff,
528            fulfillment_lib,
529            request_lib,
530            selection_ou,
531            selection_depth,
532            pickup_lib,
533            hold_type,
534            holdable_formats,
535            phone_notify,
536            email_notify,
537            sms_notify,
538            frozen,
539            thaw_date,
540            shelf_time,
541            cut_in_line,
542            mint_condition,
543            shelf_expire_time,
544            current_shelf_lib,
545            behind_desk
546         FROM action.all_hold_request WHERE id = OLD.id;
547
548     RETURN OLD;
549 END;
550 $$ LANGUAGE 'plpgsql';
551
552
553 COMMIT;