]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/version-upgrade/2.0.5-2.0.6-upgrade-db.sql
Merge branch 'master' of git.evergreen-ils.org:Evergreen-DocBook into doc_consolidati...
[Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.0.5-2.0.6-upgrade-db.sql
1 BEGIN;
2
3 INSERT INTO config.upgrade_log (version) VALUES ('2.0.6');
4 INSERT INTO config.upgrade_log (version) VALUES ('0508'); -- gmc
5
6 CREATE OR REPLACE FUNCTION maintain_901 () RETURNS TRIGGER AS $func$
7 DECLARE
8     use_id_for_tcn BOOLEAN;
9     norm_tcn_value TEXT;
10     norm_tcn_source TEXT;
11 BEGIN
12     -- Remove any existing 901 fields before we insert the authoritative one
13     NEW.marc := REGEXP_REPLACE(NEW.marc, E'<datafield[^>]*?tag="901".+?</datafield>', '', 'g');
14
15     IF TG_TABLE_SCHEMA = 'biblio' THEN
16         -- Set TCN value to record ID?
17         SELECT enabled FROM config.global_flag INTO use_id_for_tcn
18             WHERE name = 'cat.bib.use_id_for_tcn';
19
20         IF use_id_for_tcn = 't' THEN
21             NEW.tcn_value := NEW.id;
22             norm_tcn_value := NEW.tcn_value;
23         ELSE
24             -- yes, ampersands can show up in tcn_values ...
25             norm_tcn_value := REGEXP_REPLACE(NEW.tcn_value, E'&(?!\\S+;)', '&amp;', 'g');
26         END IF;
27         -- ... and TCN sources
28         -- FIXME we have here yet another (stub) version of entityize
29         norm_tcn_source := REGEXP_REPLACE(NEW.tcn_source, E'&(?!\\S+;)', '&amp;', 'g');
30
31         NEW.marc := REGEXP_REPLACE(
32             NEW.marc,
33             E'(</(?:[^:]*?:)?record>)',
34             E'<datafield tag="901" ind1=" " ind2=" ">' ||
35                 '<subfield code="a">' || norm_tcn_value || E'</subfield>' ||
36                 '<subfield code="b">' || norm_tcn_source || E'</subfield>' ||
37                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
38                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
39                 CASE WHEN NEW.owner IS NOT NULL THEN '<subfield code="o">' || NEW.owner || E'</subfield>' ELSE '' END ||
40                 CASE WHEN NEW.share_depth IS NOT NULL THEN '<subfield code="d">' || NEW.share_depth || E'</subfield>' ELSE '' END ||
41              E'</datafield>\\1'
42         );
43     ELSIF TG_TABLE_SCHEMA = 'authority' THEN
44         NEW.marc := REGEXP_REPLACE(
45             NEW.marc,
46             E'(</(?:[^:]*?:)?record>)',
47             E'<datafield tag="901" ind1=" " ind2=" ">' ||
48                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
49                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
50              E'</datafield>\\1'
51         );
52     ELSIF TG_TABLE_SCHEMA = 'serial' THEN
53         NEW.marc := REGEXP_REPLACE(
54             NEW.marc,
55             E'(</(?:[^:]*?:)?record>)',
56             E'<datafield tag="901" ind1=" " ind2=" ">' ||
57                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
58                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
59                 '<subfield code="o">' || NEW.owning_lib || E'</subfield>' ||
60                 CASE WHEN NEW.record IS NOT NULL THEN '<subfield code="r">' || NEW.record || E'</subfield>' ELSE '' END ||
61              E'</datafield>\\1'
62         );
63     ELSE
64         NEW.marc := REGEXP_REPLACE(
65             NEW.marc,
66             E'(</(?:[^:]*?:)?record>)',
67             E'<datafield tag="901" ind1=" " ind2=" ">' ||
68                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
69                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
70              E'</datafield>\\1'
71         );
72     END IF;
73
74     RETURN NEW;
75 END;
76 $func$ LANGUAGE PLPGSQL;
77
78 INSERT INTO config.upgrade_log (version) VALUES ('0509'); -- gmc
79
80 CREATE OR REPLACE FUNCTION evergreen.xml_escape(str TEXT) RETURNS text AS $$
81     SELECT REPLACE(REPLACE(REPLACE($1,
82        '&', '&amp;'),
83        '<', '&lt;'),
84        '>', '&gt;');
85 $$ LANGUAGE SQL IMMUTABLE;
86
87 CREATE OR REPLACE FUNCTION maintain_901 () RETURNS TRIGGER AS $func$
88 DECLARE
89     use_id_for_tcn BOOLEAN;
90 BEGIN
91     -- Remove any existing 901 fields before we insert the authoritative one
92     NEW.marc := REGEXP_REPLACE(NEW.marc, E'<datafield[^>]*?tag="901".+?</datafield>', '', 'g');
93
94     IF TG_TABLE_SCHEMA = 'biblio' THEN
95         -- Set TCN value to record ID?
96         SELECT enabled FROM config.global_flag INTO use_id_for_tcn
97             WHERE name = 'cat.bib.use_id_for_tcn';
98
99         IF use_id_for_tcn = 't' THEN
100             NEW.tcn_value := NEW.id;
101         END IF;
102
103         NEW.marc := REGEXP_REPLACE(
104             NEW.marc,
105             E'(</(?:[^:]*?:)?record>)',
106             E'<datafield tag="901" ind1=" " ind2=" ">' ||
107                 '<subfield code="a">' || evergreen.xml_escape(NEW.tcn_value) || E'</subfield>' ||
108                 '<subfield code="b">' || evergreen.xml_escape(NEW.tcn_source) || E'</subfield>' ||
109                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
110                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
111                 CASE WHEN NEW.owner IS NOT NULL THEN '<subfield code="o">' || NEW.owner || E'</subfield>' ELSE '' END ||
112                 CASE WHEN NEW.share_depth IS NOT NULL THEN '<subfield code="d">' || NEW.share_depth || E'</subfield>' ELSE '' END ||
113              E'</datafield>\\1'
114         );
115     ELSIF TG_TABLE_SCHEMA = 'authority' THEN
116         NEW.marc := REGEXP_REPLACE(
117             NEW.marc,
118             E'(</(?:[^:]*?:)?record>)',
119             E'<datafield tag="901" ind1=" " ind2=" ">' ||
120                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
121                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
122              E'</datafield>\\1'
123         );
124     ELSIF TG_TABLE_SCHEMA = 'serial' THEN
125         NEW.marc := REGEXP_REPLACE(
126             NEW.marc,
127             E'(</(?:[^:]*?:)?record>)',
128             E'<datafield tag="901" ind1=" " ind2=" ">' ||
129                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
130                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
131                 '<subfield code="o">' || NEW.owning_lib || E'</subfield>' ||
132                 CASE WHEN NEW.record IS NOT NULL THEN '<subfield code="r">' || NEW.record || E'</subfield>' ELSE '' END ||
133              E'</datafield>\\1'
134         );
135     ELSE
136         NEW.marc := REGEXP_REPLACE(
137             NEW.marc,
138             E'(</(?:[^:]*?:)?record>)',
139             E'<datafield tag="901" ind1=" " ind2=" ">' ||
140                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
141                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
142              E'</datafield>\\1'
143         );
144     END IF;
145
146     RETURN NEW;
147 END;
148 $func$ LANGUAGE PLPGSQL;
149
150 INSERT INTO config.upgrade_log (version) VALUES ('0510'); -- miker
151
152 SELECT evergreen.change_db_setting('search_path', ARRAY['evergreen','public','pg_catalog']);
153
154 -- Fix function breakage due to short search path
155 CREATE OR REPLACE FUNCTION evergreen.force_unicode_normal_form(string TEXT, form TEXT) RETURNS TEXT AS $func$
156 use Unicode::Normalize 'normalize';
157 return normalize($_[1],$_[0]); # reverse the params
158 $func$ LANGUAGE PLPERLU;
159
160 CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
161 BEGIN
162     NEW.value := force_unicode_normal_form(NEW.value,'NFC');
163     RETURN NEW;
164 END;
165 $$ LANGUAGE PLPGSQL;
166
167 DROP TRIGGER facet_force_nfc_tgr ON metabib.facet_entry;
168
169 CREATE TRIGGER facet_force_nfc_tgr
170     BEFORE UPDATE OR INSERT ON metabib.facet_entry
171     FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
172
173 DROP FUNCTION IF EXISTS public.force_unicode_normal_form (TEXT,TEXT);
174 DROP FUNCTION IF EXISTS public.facet_force_nfc ();
175
176 CREATE OR REPLACE FUNCTION evergreen.xml_escape(str TEXT) RETURNS text AS $$
177     SELECT REPLACE(REPLACE(REPLACE($1,
178        '&', '&amp;'),
179        '<', '&lt;'),
180        '>', '&gt;');
181 $$ LANGUAGE SQL IMMUTABLE;
182
183 CREATE OR REPLACE FUNCTION evergreen.maintain_901 () RETURNS TRIGGER AS $func$
184 DECLARE
185     use_id_for_tcn BOOLEAN;
186 BEGIN
187     -- Remove any existing 901 fields before we insert the authoritative one
188     NEW.marc := REGEXP_REPLACE(NEW.marc, E'<datafield[^>]*?tag="901".+?</datafield>', '', 'g');
189
190     IF TG_TABLE_SCHEMA = 'biblio' THEN
191         -- Set TCN value to record ID?
192         SELECT enabled FROM config.global_flag INTO use_id_for_tcn
193             WHERE name = 'cat.bib.use_id_for_tcn';
194
195         IF use_id_for_tcn = 't' THEN
196             NEW.tcn_value := NEW.id;
197         END IF;
198
199         NEW.marc := REGEXP_REPLACE(
200             NEW.marc,
201             E'(</(?:[^:]*?:)?record>)',
202             E'<datafield tag="901" ind1=" " ind2=" ">' ||
203                 '<subfield code="a">' || evergreen.xml_escape(NEW.tcn_value) || E'</subfield>' ||
204                 '<subfield code="b">' || evergreen.xml_escape(NEW.tcn_source) || E'</subfield>' ||
205                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
206                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
207                 CASE WHEN NEW.owner IS NOT NULL THEN '<subfield code="o">' || NEW.owner || E'</subfield>' ELSE '' END ||
208                 CASE WHEN NEW.share_depth IS NOT NULL THEN '<subfield code="d">' || NEW.share_depth || E'</subfield>' ELSE '' END ||
209              E'</datafield>\\1'
210         );
211     ELSIF TG_TABLE_SCHEMA = 'authority' THEN
212         NEW.marc := REGEXP_REPLACE(
213             NEW.marc,
214             E'(</(?:[^:]*?:)?record>)',
215             E'<datafield tag="901" ind1=" " ind2=" ">' ||
216                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
217                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
218              E'</datafield>\\1'
219         );
220     ELSIF TG_TABLE_SCHEMA = 'serial' THEN
221         NEW.marc := REGEXP_REPLACE(
222             NEW.marc,
223             E'(</(?:[^:]*?:)?record>)',
224             E'<datafield tag="901" ind1=" " ind2=" ">' ||
225                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
226                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
227                 '<subfield code="o">' || NEW.owning_lib || E'</subfield>' ||
228                 CASE WHEN NEW.record IS NOT NULL THEN '<subfield code="r">' || NEW.record || E'</subfield>' ELSE '' END ||
229              E'</datafield>\\1'
230         );
231     ELSE
232         NEW.marc := REGEXP_REPLACE(
233             NEW.marc,
234             E'(</(?:[^:]*?:)?record>)',
235             E'<datafield tag="901" ind1=" " ind2=" ">' ||
236                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
237                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
238              E'</datafield>\\1'
239         );
240     END IF;
241
242     RETURN NEW;
243 END;
244 $func$ LANGUAGE PLPGSQL;
245
246 DROP TRIGGER b_maintain_901 ON biblio.record_entry;
247 DROP TRIGGER b_maintain_901 ON authority.record_entry;
248 DROP TRIGGER b_maintain_901 ON serial.record_entry;
249
250 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
251 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
252 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON serial.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
253
254 DROP FUNCTION IF EXISTS public.maintain_901 ();
255
256 INSERT INTO config.upgrade_log (version) VALUES ('0511'); -- miker
257
258 CREATE OR REPLACE FUNCTION evergreen.fake_fkey_tgr () RETURNS TRIGGER AS $F$
259 DECLARE
260     copy_id BIGINT;
261 BEGIN
262     EXECUTE 'SELECT ($1).' || quote_ident(TG_ARGV[0]) INTO copy_id USING NEW;
263     PERFORM * FROM asset.copy WHERE id = copy_id;
264     IF NOT FOUND THEN
265         RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
266     END IF;
267     RETURN NULL;
268 END;
269 $F$ LANGUAGE PLPGSQL;
270
271 CREATE TRIGGER action_circulation_target_copy_trig AFTER INSERT OR UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE evergreen.fake_fkey_tgr('target_copy');
272
273 INSERT INTO config.upgrade_log (version) VALUES ('0516'); 
274
275 CREATE OR REPLACE FUNCTION public.extract_acq_marc_field ( BIGINT, TEXT, TEXT) RETURNS TEXT AS $$    
276     SELECT extract_marc_field('acq.lineitem', $1, $2, $3);
277 $$ LANGUAGE SQL;
278
279 INSERT INTO config.upgrade_log (version) VALUES ('0517'); --miker
280
281 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
282 DECLARE
283     uris            TEXT[];
284     uri_xml         TEXT;
285     uri_label       TEXT;
286     uri_href        TEXT;
287     uri_use         TEXT;
288     uri_owner_list  TEXT[];
289     uri_owner       TEXT;
290     uri_owner_id    INT;
291     uri_id          INT;
292     uri_cn_id       INT;
293     uri_map_id      INT;
294 BEGIN
295
296     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
297     IF ARRAY_UPPER(uris,1) > 0 THEN
298         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
299             -- First we pull info out of the 856
300             uri_xml     := uris[i];
301
302             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
303             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()|//*[@code="u"]/text()',uri_xml))[1];
304             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()|//*[@code="u"]/text()',uri_xml))[1];
305             CONTINUE WHEN uri_href IS NULL OR uri_label IS NULL;
306
307             -- Get the distinct list of libraries wanting to use 
308             SELECT  ARRAY_ACCUM(
309                         DISTINCT REGEXP_REPLACE(
310                             x,
311                             $re$^.*?\((\w+)\).*$$re$,
312                             E'\\1'
313                         )
314                     ) INTO uri_owner_list
315               FROM  UNNEST(
316                         oils_xpath(
317                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
318                             uri_xml
319                         )
320                     )x;
321
322             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
323
324                 -- look for a matching uri
325                 SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
326                 IF NOT FOUND THEN -- create one
327                     INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
328                     SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
329                 END IF;
330
331                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
332                     uri_owner := uri_owner_list[j];
333
334                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
335                     CONTINUE WHEN NOT FOUND;
336
337                     -- we need a call number to link through
338                     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;
339                     IF NOT FOUND THEN
340                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
341                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
342                         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;
343                     END IF;
344
345                     -- now, link them if they're not already
346                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
347                     IF NOT FOUND THEN
348                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
349                     END IF;
350
351                 END LOOP;
352
353             END IF;
354
355         END LOOP;
356     END IF;
357
358     RETURN;
359 END;
360 $func$ LANGUAGE PLPGSQL;
361
362 INSERT INTO config.upgrade_log (version) VALUES ('0520'); --dbs
363 INSERT INTO config.upgrade_log (version) VALUES ('0521'); --dbs
364
365 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
366 DECLARE
367     uris            TEXT[];
368     uri_xml         TEXT;
369     uri_label       TEXT;
370     uri_href        TEXT;
371     uri_use         TEXT;
372     uri_owner_list  TEXT[];
373     uri_owner       TEXT;
374     uri_owner_id    INT;
375     uri_id          INT;
376     uri_cn_id       INT;
377     uri_map_id      INT;
378 BEGIN
379
380     -- Clear any URI mappings and call numbers for this bib.
381     -- This leads to acn / auricnm inflation, but also enables
382     -- old acn/auricnm's to go away and for bibs to be deleted.
383     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
384         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
385         DELETE FROM asset.call_number WHERE id = uri_cn_id;
386     END LOOP;
387
388     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
389     IF ARRAY_UPPER(uris,1) > 0 THEN
390         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
391             -- First we pull info out of the 856
392             uri_xml     := uris[i];
393
394             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
395             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()|//*[@code="u"]/text()',uri_xml))[1];
396             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
397             CONTINUE WHEN uri_href IS NULL OR uri_label IS NULL;
398
399             -- Get the distinct list of libraries wanting to use 
400             SELECT  ARRAY_ACCUM(
401                         DISTINCT REGEXP_REPLACE(
402                             x,
403                             $re$^.*?\((\w+)\).*$$re$,
404                             E'\\1'
405                         )
406                     ) INTO uri_owner_list
407               FROM  UNNEST(
408                         oils_xpath(
409                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
410                             uri_xml
411                         )
412                     )x;
413
414             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
415
416                 -- look for a matching uri
417                 SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
418                 IF NOT FOUND THEN -- create one
419                     INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
420                     IF uri_use IS NULL THEN
421                         SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
422                     ELSE
423                         SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
424                     END IF;
425                 END IF;
426
427                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
428                     uri_owner := uri_owner_list[j];
429
430                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
431                     CONTINUE WHEN NOT FOUND;
432
433                     -- we need a call number to link through
434                     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;
435                     IF NOT FOUND THEN
436                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
437                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
438                         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;
439                     END IF;
440
441                     -- now, link them if they're not already
442                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
443                     IF NOT FOUND THEN
444                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
445                     END IF;
446
447                 END LOOP;
448
449             END IF;
450
451         END LOOP;
452     END IF;
453
454     RETURN;
455 END;
456 $func$ LANGUAGE PLPGSQL;
457
458 INSERT INTO config.upgrade_log (version) VALUES ('0528'); -- dbs
459
460 CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
461 use strict;
462 use MARC::Record;
463 use MARC::File::XML (BinaryEncoding => 'UTF-8');
464 use MARC::Charset;
465 use Encode;
466 use Unicode::Normalize;
467
468 MARC::Charset->assume_unicode(1);
469
470 my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
471 my $schema = $_TD->{table_schema};
472 my $rec_id = $_TD->{new}{id};
473
474 # Short-circuit if maintaining control numbers per MARC21 spec is not enabled
475 my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
476 if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
477     return;
478 }
479
480 # Get the control number identifier from an OU setting based on $_TD->{new}{owner}
481 my $ou_cni = 'EVRGRN';
482
483 my $owner;
484 if ($schema eq 'serial') {
485     $owner = $_TD->{new}{owning_lib};
486 } else {
487     # are.owner and bre.owner can be null, so fall back to the consortial setting
488     $owner = $_TD->{new}{owner} || 1;
489 }
490
491 my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
492 if ($ous_rv->{processed}) {
493     $ou_cni = $ous_rv->{rows}[0]->{value};
494     $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
495 } else {
496     # Fall back to the shortname of the OU if there was no OU setting
497     $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
498     if ($ous_rv->{processed}) {
499         $ou_cni = $ous_rv->{rows}[0]->{shortname};
500     }
501 }
502
503 my ($create, $munge) = (0, 0);
504
505 my @scns = $record->field('035');
506
507 foreach my $id_field ('001', '003') {
508     my $spec_value;
509     my @controls = $record->field($id_field);
510
511     if ($id_field eq '001') {
512         $spec_value = $rec_id;
513     } else {
514         $spec_value = $ou_cni;
515     }
516
517     # Create the 001/003 if none exist
518     if (scalar(@controls) == 1) {
519         # Only one field; check to see if we need to munge it
520         unless (grep $_->data() eq $spec_value, @controls) {
521             $munge = 1;
522         }
523     } else {
524         # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
525         foreach my $control (@controls) {
526             unless ($control->data() eq $spec_value) {
527                 $record->delete_field($control);
528             }
529         }
530         $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
531         $create = 1;
532     }
533 }
534
535 # Now, if we need to munge the 001, we will first push the existing 001/003
536 # into the 035; but if the record did not have one (and one only) 001 and 003
537 # to begin with, skip this process
538 if ($munge and not $create) {
539     my $scn = "(" . $record->field('003')->data() . ")" . $record->field('001')->data();
540
541     # Do not create duplicate 035 fields
542     unless (grep $_->subfield('a') eq $scn, @scns) {
543         $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
544     }
545 }
546
547 # Set the 001/003 and update the MARC
548 if ($create or $munge) {
549     $record->field('001')->data($rec_id);
550     $record->field('003')->data($ou_cni);
551
552     my $xml = $record->as_xml_record();
553     $xml =~ s/\n//sgo;
554     $xml =~ s/^<\?xml.+\?\s*>//go;
555     $xml =~ s/>\s+</></go;
556     $xml =~ s/\p{Cc}//go;
557
558     # Embed a version of OpenILS::Application::AppUtils->entityize()
559     # to avoid having to set PERL5LIB for PostgreSQL as well
560
561     # If we are going to convert non-ASCII characters to XML entities,
562     # we had better be dealing with a UTF8 string to begin with
563     $xml = decode_utf8($xml);
564
565     $xml = NFC($xml);
566
567     # Convert raw ampersands to entities
568     $xml =~ s/&(?!\S+;)/&amp;/gso;
569
570     # Convert Unicode characters to entities
571     $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
572
573     $xml =~ s/[\x00-\x1f]//go;
574     $_TD->{new}{marc} = $xml;
575
576     return "MODIFY";
577 }
578
579 return;
580 $func$ LANGUAGE PLPERLU;
581
582 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT ) RETURNS TEXT AS $func$
583
584     use MARC::Record;
585     use MARC::File::XML (BinaryEncoding => 'UTF-8');
586     use MARC::Charset;
587
588     MARC::Charset->assume_unicode(1);
589
590     my $xml = shift;
591     my $r = MARC::Record->new_from_xml( $xml );
592
593     return undef unless ($r);
594
595     my $id = shift() || $r->subfield( '901' => 'c' );
596     $id =~ s/^\s*(?:\([^)]+\))?\s*(.+)\s*?$/$1/;
597     return undef unless ($id); # We need an ID!
598
599     my $tmpl = MARC::Record->new();
600     $tmpl->encoding( 'UTF-8' );
601
602     my @rule_fields;
603     for my $field ( $r->field( '1..' ) ) { # Get main entry fields from the authority record
604
605         my $tag = $field->tag;
606         my $i1 = $field->indicator(1);
607         my $i2 = $field->indicator(2);
608         my $sf = join '', map { $_->[0] } $field->subfields;
609         my @data = map { @$_ } $field->subfields;
610
611         my @replace_them;
612
613         # Map the authority field to bib fields it can control.
614         if ($tag >= 100 and $tag <= 111) {       # names
615             @replace_them = map { $tag + $_ } (0, 300, 500, 600, 700);
616         } elsif ($tag eq '130') {                # uniform title
617             @replace_them = qw/130 240 440 730 830/;
618         } elsif ($tag >= 150 and $tag <= 155) {  # subjects
619             @replace_them = ($tag + 500);
620         } elsif ($tag >= 180 and $tag <= 185) {  # floating subdivisions
621             @replace_them = qw/100 400 600 700 800 110 410 610 710 810 111 411 611 711 811 130 240 440 730 830 650 651 655/;
622         } else {
623             next;
624         }
625
626         # Dummy up the bib-side data
627         $tmpl->append_fields(
628             map {
629                 MARC::Field->new( $_, $i1, $i2, @data )
630             } @replace_them
631         );
632
633         # Construct some 'replace' rules
634         push @rule_fields, map { $_ . $sf . '[0~\)' .$id . '$]' } @replace_them;
635     }
636
637     # Insert the replace rules into the template
638     $tmpl->append_fields(
639         MARC::Field->new( '905' => ' ' => ' ' => 'r' => join(',', @rule_fields ) )
640     );
641
642     $xml = $tmpl->as_xml_record;
643     $xml =~ s/^<\?.+?\?>$//mo;
644     $xml =~ s/\n//sgo;
645     $xml =~ s/>\s+</></sgo;
646
647     return $xml;
648
649 $func$ LANGUAGE PLPERLU;
650
651 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
652
653     use MARC::Record;
654     use MARC::File::XML (BinaryEncoding => 'UTF-8');
655     use MARC::Charset;
656     use strict;
657
658     MARC::Charset->assume_unicode(1);
659
660     my $target_xml = shift;
661     my $source_xml = shift;
662     my $field_spec = shift;
663     my $force_add = shift || 0;
664
665     my $target_r = MARC::Record->new_from_xml( $target_xml );
666     my $source_r = MARC::Record->new_from_xml( $source_xml );
667
668     return $target_xml unless ($target_r && $source_r);
669
670     my @field_list = split(',', $field_spec);
671
672     my %fields;
673     for my $f (@field_list) {
674         $f =~ s/^\s*//; $f =~ s/\s*$//;
675         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
676             my $field = $1;
677             $field =~ s/\s+//;
678             my $sf = $2;
679             $sf =~ s/\s+//;
680             my $match = $3;
681             $match =~ s/^\s*//; $match =~ s/\s*$//;
682             $fields{$field} = { sf => [ split('', $sf) ] };
683             if ($match) {
684                 my ($msf,$mre) = split('~', $match);
685                 if (length($msf) > 0 and length($mre) > 0) {
686                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
687                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
688                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
689                 }
690             }
691         }
692     }
693
694     for my $f ( keys %fields) {
695         if ( @{$fields{$f}{sf}} ) {
696             for my $from_field ($source_r->field( $f )) {
697                 my @tos = $target_r->field( $f );
698                 if (!@tos) {
699                     next if (exists($fields{$f}{match}) and !$force_add);
700                     my @new_fields = map { $_->clone } $source_r->field( $f );
701                     $target_r->insert_fields_ordered( @new_fields );
702                 } else {
703                     for my $to_field (@tos) {
704                         if (exists($fields{$f}{match})) {
705                             next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
706                         }
707                         my @new_sf = map { ($_ => $from_field->subfield($_)) } @{$fields{$f}{sf}};
708                         $to_field->add_subfields( @new_sf );
709                     }
710                 }
711             }
712         } else {
713             my @new_fields = map { $_->clone } $source_r->field( $f );
714             $target_r->insert_fields_ordered( @new_fields );
715         }
716     }
717
718     $target_xml = $target_r->as_xml_record;
719     $target_xml =~ s/^<\?.+?\?>$//mo;
720     $target_xml =~ s/\n//sgo;
721     $target_xml =~ s/>\s+</></sgo;
722
723     return $target_xml;
724
725 $_$ LANGUAGE PLPERLU;
726
727 CREATE OR REPLACE FUNCTION authority.normalize_heading( TEXT ) RETURNS TEXT AS $func$
728     use strict;
729     use warnings;
730
731     use utf8;
732     use MARC::Record;
733     use MARC::File::XML (BinaryEncoding => 'UTF8');
734     use MARC::Charset;
735     use UUID::Tiny ':std';
736
737     MARC::Charset->assume_unicode(1);
738
739     my $xml = shift() or return undef;
740
741     my $r;
742
743     # Prevent errors in XML parsing from blowing out ungracefully
744     eval {
745         $r = MARC::Record->new_from_xml( $xml );
746         1;
747     } or do {
748        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
749     };
750
751     if (!$r) {
752        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
753     }
754
755     # From http://www.loc.gov/standards/sourcelist/subject.html
756     my $thes_code_map = {
757         a => 'lcsh',
758         b => 'lcshac',
759         c => 'mesh',
760         d => 'nal',
761         k => 'cash',
762         n => 'notapplicable',
763         r => 'aat',
764         s => 'sears',
765         v => 'rvm',
766     };
767
768     # Default to "No attempt to code" if the leader is horribly broken
769     my $fixed_field = $r->field('008');
770     my $thes_char = '|';
771     if ($fixed_field) { 
772         $thes_char = substr($fixed_field->data(), 11, 1) || '|';
773     }
774
775     my $thes_code = 'UNDEFINED';
776
777     if ($thes_char eq 'z') {
778         # Grab the 040 $f per http://www.loc.gov/marc/authority/ad040.html
779         $thes_code = $r->subfield('040', 'f') || 'UNDEFINED';
780     } elsif ($thes_code_map->{$thes_char}) {
781         $thes_code = $thes_code_map->{$thes_char};
782     }
783
784     my $auth_txt = '';
785     my $head = $r->field('1..');
786     if ($head) {
787         # Concatenate all of these subfields together, prefixed by their code
788         # to prevent collisions along the lines of "Fiction, North Carolina"
789         foreach my $sf ($head->subfields()) {
790             $auth_txt .= '‡' . $sf->[0] . ' ' . $sf->[1];
791         }
792     }
793     
794     if ($auth_txt) {
795         my $stmt = spi_prepare('SELECT public.naco_normalize($1) AS norm_text', 'TEXT');
796         my $result = spi_exec_prepared($stmt, $auth_txt);
797         my $norm_txt = $result->{rows}[0]->{norm_text};
798         spi_freeplan($stmt);
799         undef($stmt);
800         return $head->tag() . "_" . $thes_code . " " . $norm_txt;
801     }
802
803     return 'NOHEADING_' . $thes_code . ' ' . create_uuid_as_string(UUID_MD5, $xml);
804 $func$ LANGUAGE 'plperlu' IMMUTABLE;
805
806 CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
807
808     use MARC::Record;
809     use MARC::File::XML (BinaryEncoding => 'UTF-8');
810     use MARC::Charset;
811     use strict;
812
813     MARC::Charset->assume_unicode(1);
814
815     my $xml = shift;
816     my $r = MARC::Record->new_from_xml( $xml );
817
818     return $xml unless ($r);
819
820     my $field_spec = shift;
821     my @field_list = split(',', $field_spec);
822
823     my %fields;
824     for my $f (@field_list) {
825         $f =~ s/^\s*//; $f =~ s/\s*$//;
826         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
827             my $field = $1;
828             $field =~ s/\s+//;
829             my $sf = $2;
830             $sf =~ s/\s+//;
831             my $match = $3;
832             $match =~ s/^\s*//; $match =~ s/\s*$//;
833             $fields{$field} = { sf => [ split('', $sf) ] };
834             if ($match) {
835                 my ($msf,$mre) = split('~', $match);
836                 if (length($msf) > 0 and length($mre) > 0) {
837                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
838                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
839                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
840                 }
841             }
842         }
843     }
844
845     for my $f ( keys %fields) {
846         for my $to_field ($r->field( $f )) {
847             if (exists($fields{$f}{match})) {
848                 next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
849             }
850
851             if ( @{$fields{$f}{sf}} ) {
852                 $to_field->delete_subfield(code => $fields{$f}{sf});
853             } else {
854                 $r->delete_field( $to_field );
855             }
856         }
857     }
858
859     $xml = $r->as_xml_record;
860     $xml =~ s/^<\?.+?\?>$//mo;
861     $xml =~ s/\n//sgo;
862     $xml =~ s/>\s+</></sgo;
863
864     return $xml;
865
866 $_$ LANGUAGE PLPERLU;
867
868 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
869
870 use MARC::Record;
871 use MARC::File::XML (BinaryEncoding => 'UTF-8');
872 use MARC::Charset;
873
874 MARC::Charset->assume_unicode(1);
875
876 my $xml = shift;
877 my $r = MARC::Record->new_from_xml( $xml );
878
879 return_next( { tag => 'LDR', value => $r->leader } );
880
881 for my $f ( $r->fields ) {
882         if ($f->is_control_field) {
883                 return_next({ tag => $f->tag, value => $f->data });
884         } else {
885                 for my $s ($f->subfields) {
886                         return_next({
887                                 tag      => $f->tag,
888                                 ind1     => $f->indicator(1),
889                                 ind2     => $f->indicator(2),
890                                 subfield => $s->[0],
891                                 value    => $s->[1]
892                         });
893
894                         if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
895                                 my $trim = $f->indicator(2) || 0;
896                                 return_next({
897                                         tag      => 'tnf',
898                                         ind1     => $f->indicator(1),
899                                         ind2     => $f->indicator(2),
900                                         subfield => 'a',
901                                         value    => substr( $s->[1], $trim )
902                                 });
903                         }
904                 }
905         }
906 }
907
908 return undef;
909
910 $func$ LANGUAGE PLPERLU;
911
912 CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authority.full_rec AS $func$
913
914 use MARC::Record;
915 use MARC::File::XML (BinaryEncoding => 'UTF-8');
916 use MARC::Charset;
917
918 MARC::Charset->assume_unicode(1);
919
920 my $xml = shift;
921 my $r = MARC::Record->new_from_xml( $xml );
922
923 return_next( { tag => 'LDR', value => $r->leader } );
924
925 for my $f ( $r->fields ) {
926     if ($f->is_control_field) {
927         return_next({ tag => $f->tag, value => $f->data });
928     } else {
929         for my $s ($f->subfields) {
930             return_next({
931                 tag      => $f->tag,
932                 ind1     => $f->indicator(1),
933                 ind2     => $f->indicator(2),
934                 subfield => $s->[0],
935                 value    => $s->[1]
936             });
937
938         }
939     }
940 }
941
942 return undef;
943
944 $func$ LANGUAGE PLPERLU;
945
946 INSERT INTO config.upgrade_log (version) VALUES ('0529');
947
948 INSERT INTO config.org_unit_setting_type 
949 ( name, label, description, datatype ) VALUES 
950 ( 'circ.user_merge.delete_addresses', 
951   'Circ:  Patron Merge Address Delete', 
952   'Delete address(es) of subordinate user(s) in a patron merge', 
953    'bool'
954 );
955
956 INSERT INTO config.org_unit_setting_type 
957 ( name, label, description, datatype ) VALUES 
958 ( 'circ.user_merge.delete_cards', 
959   'Circ: Patron Merge Barcode Delete', 
960   'Delete barcode(s) of subordinate user(s) in a patron merge', 
961   'bool'
962 );
963
964 INSERT INTO config.org_unit_setting_type 
965 ( name, label, description, datatype ) VALUES 
966 ( 'circ.user_merge.deactivate_cards', 
967   'Circ:  Patron Merge Deactivate Card', 
968   'Mark barcode(s) of subordinate user(s) in a patron merge as inactive', 
969   'bool'
970 );
971
972 COMMIT;