LP#1205061: Add IF EXISTS to some DROP statements in 2.3-2.4 upgrade script
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.3-2.4.0RC-upgrade-db.sql
1 --Upgrade Script for 2.3 to 2.4.0RC
2 \set eg_version '''2.4.0RC'''
3 BEGIN;
4 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('2.4.0RC', :eg_version);
5 -- remove the Bypass hold capture during clear shelf process setting
6
7 SELECT evergreen.upgrade_deps_block_check('0739', :eg_version);
8
9
10 DELETE FROM actor.org_unit_setting WHERE name = 'circ.holds.clear_shelf.no_capture_holds';
11 DELETE FROM config.org_unit_setting_type_log WHERE field_name = 'circ.holds.clear_shelf.no_capture_holds';
12
13
14 DELETE FROM config.org_unit_setting_type WHERE name = 'circ.holds.clear_shelf.no_capture_holds';
15
16
17 SELECT evergreen.upgrade_deps_block_check('0741', :eg_version);
18
19 INSERT INTO permission.perm_list ( id, code, description ) VALUES (
20     540,
21     'ADMIN_TOOLBAR_FOR_ORG',
22     oils_i18n_gettext(
23         540,
24         'Allows a user to create, edit, and delete custom toolbars for org units',
25         'ppl',
26         'description'
27     )
28 ), (
29     541,
30     'ADMIN_TOOLBAR_FOR_WORKSTATION',
31     oils_i18n_gettext(
32         541,
33         'Allows a user to create, edit, and delete custom toolbars for workstations',
34         'ppl',
35         'description'
36     )
37 ), (
38     542,
39     'ADMIN_TOOLBAR_FOR_USER',
40     oils_i18n_gettext(
41         542,
42         'Allows a user to create, edit, and delete custom toolbars for users',
43         'ppl',
44         'description'
45     )
46 );
47
48
49 -- Evergreen DB patch 0743.schema.remove_tsearch2.sql
50 --
51 -- Enable native full-text search to be used, and drop TSearch2 extension
52 --
53
54 -- check whether patch can be applied
55 SELECT evergreen.upgrade_deps_block_check('0743', :eg_version);
56
57 -- FIXME: add/check SQL statements to perform the upgrade
58 -- First up, these functions depend on metabib.full_rec. They have to go for now.
59 DROP FUNCTION IF EXISTS biblio.flatten_marc(bigint);
60 DROP FUNCTION IF EXISTS biblio.flatten_marc(text);
61
62 -- These views depend on metabib.full_rec as well. Bye-bye!
63 DROP VIEW IF EXISTS reporter.old_super_simple_record;
64 DROP VIEW IF EXISTS reporter.simple_record;
65 DROP VIEW IF EXISTS reporter.classic_item_list;
66
67 \echo WARNING: The reporter.classic_item_list view was dropped if it existed.
68 \echo If you use that view, please run the example.reporter-extension.sql script
69 \echo to recreate it after rest of the schema upgrade is complete.
70
71 -- Now we can drop metabib.full_rec.
72 DROP VIEW IF EXISTS metabib.full_rec;
73
74 -- These indexes have to go. BEFORE we alter the tables, otherwise things take extra time when we alter the tables.
75 DROP INDEX metabib.metabib_author_field_entry_value_idx;
76 DROP INDEX metabib.metabib_identifier_field_entry_value_idx;
77 DROP INDEX metabib.metabib_keyword_field_entry_value_idx;
78 DROP INDEX metabib.metabib_series_field_entry_value_idx;
79 DROP INDEX metabib.metabib_subject_field_entry_value_idx;
80 DROP INDEX metabib.metabib_title_field_entry_value_idx;
81
82 -- Now grab all of the tsvector-enabled columns and switch them to the non-wrapper version of the type.
83 ALTER TABLE authority.full_rec ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
84 ALTER TABLE authority.simple_heading ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
85 ALTER TABLE metabib.real_full_rec ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
86 ALTER TABLE metabib.author_field_entry ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
87 ALTER TABLE metabib.browse_entry ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
88 ALTER TABLE metabib.identifier_field_entry ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
89 ALTER TABLE metabib.keyword_field_entry ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
90 ALTER TABLE metabib.series_field_entry ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
91 ALTER TABLE metabib.subject_field_entry ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
92 ALTER TABLE metabib.title_field_entry ALTER COLUMN index_vector TYPE pg_catalog.tsvector;
93
94 -- Make sure that tsearch2 exists as an extension (for a sufficiently
95 -- old Evergreen database, it might still be an unpackaged contrib).
96 CREATE EXTENSION IF NOT EXISTS tsearch2 SCHEMA public FROM unpackaged;
97 -- Halfway there! Goodbye tsearch2 extension!
98 DROP EXTENSION tsearch2;
99
100 -- Next up, re-creating all of the stuff we just dropped.
101
102 -- Indexes! Note to whomever: Do we even need these anymore?
103 CREATE INDEX metabib_author_field_entry_value_idx ON metabib.author_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
104 CREATE INDEX metabib_identifier_field_entry_value_idx ON metabib.identifier_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
105 CREATE INDEX metabib_keyword_field_entry_value_idx ON metabib.keyword_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
106 CREATE INDEX metabib_series_field_entry_value_idx ON metabib.series_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
107 CREATE INDEX metabib_subject_field_entry_value_idx ON metabib.subject_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
108 CREATE INDEX metabib_title_field_entry_value_idx ON metabib.title_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
109
110 -- metabib.full_rec, with insert/update/delete rules
111 CREATE OR REPLACE VIEW metabib.full_rec AS
112     SELECT  id,
113             record,
114             tag,
115             ind1,
116             ind2,
117             subfield,
118             SUBSTRING(value,1,1024) AS value,
119             index_vector
120       FROM  metabib.real_full_rec;
121
122 CREATE OR REPLACE RULE metabib_full_rec_insert_rule
123     AS ON INSERT TO metabib.full_rec
124     DO INSTEAD
125     INSERT INTO metabib.real_full_rec VALUES (
126         COALESCE(NEW.id, NEXTVAL('metabib.full_rec_id_seq'::REGCLASS)),
127         NEW.record,
128         NEW.tag,
129         NEW.ind1,
130         NEW.ind2,
131         NEW.subfield,
132         NEW.value,
133         NEW.index_vector
134     );
135
136 CREATE OR REPLACE RULE metabib_full_rec_update_rule
137     AS ON UPDATE TO metabib.full_rec
138     DO INSTEAD
139     UPDATE  metabib.real_full_rec SET
140         id = NEW.id,
141         record = NEW.record,
142         tag = NEW.tag,
143         ind1 = NEW.ind1,
144         ind2 = NEW.ind2,
145         subfield = NEW.subfield,
146         value = NEW.value,
147         index_vector = NEW.index_vector
148       WHERE id = OLD.id;
149
150 CREATE OR REPLACE RULE metabib_full_rec_delete_rule
151     AS ON DELETE TO metabib.full_rec
152     DO INSTEAD
153     DELETE FROM metabib.real_full_rec WHERE id = OLD.id;
154
155 -- reporter views that depended on metabib.full_rec are up next
156 CREATE OR REPLACE VIEW reporter.simple_record AS
157 SELECT  r.id,
158     s.metarecord,
159     r.fingerprint,
160     r.quality,
161     r.tcn_source,
162     r.tcn_value,
163     title.value AS title,
164     uniform_title.value AS uniform_title,
165     author.value AS author,
166     publisher.value AS publisher,
167     SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
168     series_title.value AS series_title,
169     series_statement.value AS series_statement,
170     summary.value AS summary,
171     ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
172     ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
173     ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
174     ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
175     ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
176     ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
177     ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
178     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
179   FROM  biblio.record_entry r
180     JOIN metabib.metarecord_source_map s ON (s.source = r.id)
181     LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
182     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
183     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
184     LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
185     LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
186     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
187     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
188     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')
189     LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
190     LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
191   GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
192
193 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
194 SELECT  r.id,
195     r.fingerprint,
196     r.quality,
197     r.tcn_source,
198     r.tcn_value,
199     FIRST(title.value) AS title,
200     FIRST(author.value) AS author,
201     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT publisher.value), ', ') AS publisher,
202     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$) ), ', ') AS pubdate,
203     ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
204     ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn
205   FROM  biblio.record_entry r
206     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
207     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
208     LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
209     LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
210     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
211     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
212   GROUP BY 1,2,3,4,5;
213
214 -- And finally, the biblio functions. NOTE: I can't find the original source of the second one, so I skipped it as old cruft that was in our production DB.
215 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
216 DECLARE
217     bib biblio.record_entry%ROWTYPE;
218     output  metabib.full_rec%ROWTYPE;
219     field   RECORD;
220 BEGIN
221     SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
222
223     FOR field IN SELECT * FROM vandelay.flatten_marc( bib.marc ) LOOP
224         output.record := rid;
225         output.ind1 := field.ind1;
226         output.ind2 := field.ind2;
227         output.tag := field.tag;
228         output.subfield := field.subfield;
229         output.value := field.value;
230
231         RETURN NEXT output;
232     END LOOP;
233 END;
234 $func$ LANGUAGE PLPGSQL;
235
236 -- Evergreen DB patch 0745.data.prewarn_expire_setting.sql
237 --
238 -- Configuration setting to warn staff when an account is about to expire
239 --
240
241 -- check whether patch can be applied
242 SELECT evergreen.upgrade_deps_block_check('0745', :eg_version);
243
244 INSERT INTO config.org_unit_setting_type
245     (name, grp, label, description, datatype)
246     VALUES (
247         'circ.patron_expires_soon_warning',
248         'circ',
249         oils_i18n_gettext(
250             'circ.patron_expires_soon_warning',
251             'Warn when patron account is about to expire',
252             'coust',
253             'label'
254         ),
255         oils_i18n_gettext(
256             'circ.patron_expires_soon_warning',
257             'Warn when patron account is about to expire. If set, the staff client displays a warning this many days before the expiry of a patron account. Value is in number of days, for example: 3 for 3 days.',
258             'coust',
259             'description'
260         ),
261         'integer'
262     );
263
264 -- LP1076399: Prevent reactivated holds from canceling immediately.
265 -- Set the expire_time to NULL on all frozen/suspended holds.
266
267 SELECT evergreen.upgrade_deps_block_check('0747', :eg_version);
268
269 UPDATE action.hold_request
270 SET expire_time = NULL
271 WHERE frozen = 't'; 
272
273
274 SELECT evergreen.upgrade_deps_block_check('0752', :eg_version);
275
276 INSERT INTO container.biblio_record_entry_bucket_type (code, label) VALUES ('url_verify', 'URL Verification Queue');
277
278 DROP SCHEMA IF EXISTS url_verify CASCADE;
279
280 CREATE SCHEMA url_verify;
281
282 CREATE TABLE url_verify.session (
283     id          SERIAL                      PRIMARY KEY,
284     name        TEXT                        NOT NULL,
285     owning_lib  INT                         NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
286     creator     INT                         NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
287     container   INT                         NOT NULL REFERENCES container.biblio_record_entry_bucket (id) DEFERRABLE INITIALLY DEFERRED,
288     create_time TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
289     search      TEXT                        NOT NULL,
290     CONSTRAINT uvs_name_once_per_lib UNIQUE (name, owning_lib)
291 );
292
293 CREATE TABLE url_verify.url_selector (
294     id      SERIAL  PRIMARY KEY,
295     xpath   TEXT    NOT NULL,
296     session INT     NOT NULL REFERENCES url_verify.session (id) DEFERRABLE INITIALLY DEFERRED,
297     CONSTRAINT tag_once_per_sess UNIQUE (xpath, session)
298 );
299
300 CREATE TABLE url_verify.url (
301     id              SERIAL  PRIMARY KEY,
302     redirect_from   INT     REFERENCES url_verify.url(id) DEFERRABLE INITIALLY DEFERRED,
303     item            INT     REFERENCES container.biblio_record_entry_bucket_item (id) DEFERRABLE INITIALLY DEFERRED,
304     url_selector    INT     REFERENCES url_verify.url_selector (id) DEFERRABLE INITIALLY DEFERRED,
305     session         INT     REFERENCES url_verify.session (id) DEFERRABLE INITIALLY DEFERRED,
306     tag             TEXT,
307     subfield        TEXT,
308     ord             INT,
309     full_url        TEXT    NOT NULL,
310     scheme          TEXT,
311     username        TEXT,
312     password        TEXT,
313     host            TEXT,
314     domain          TEXT,
315     tld             TEXT,
316     port            TEXT,
317     path            TEXT,
318     page            TEXT,
319     query           TEXT,
320     fragment        TEXT,
321     CONSTRAINT redirect_or_from_item CHECK (
322         redirect_from IS NOT NULL OR (
323             item         IS NOT NULL AND
324             url_selector IS NOT NULL AND
325             tag          IS NOT NULL AND
326             subfield     IS NOT NULL AND
327             ord          IS NOT NULL
328         )
329     )
330 );
331
332 CREATE TABLE url_verify.verification_attempt (
333     id          SERIAL                      PRIMARY KEY,
334     usr         INT                         NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
335     session     INT                         NOT NULL REFERENCES url_verify.session (id) DEFERRABLE INITIALLY DEFERRED,
336     start_time  TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
337     finish_time TIMESTAMP WITH TIME ZONE
338 );
339  
340 CREATE TABLE url_verify.url_verification (
341     id          SERIAL                      PRIMARY KEY,
342     url         INT                         NOT NULL REFERENCES url_verify.url (id) DEFERRABLE INITIALLY DEFERRED,
343     attempt     INT                         NOT NULL REFERENCES url_verify.verification_attempt (id) DEFERRABLE INITIALLY DEFERRED,
344     req_time    TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
345     res_time    TIMESTAMP WITH TIME ZONE, 
346     res_code    INT                         CHECK (res_code BETWEEN 100 AND 999), -- we know > 599 will never be valid HTTP code, but we use 9XX for other stuff
347     res_text    TEXT, 
348     redirect_to INT                         REFERENCES url_verify.url (id) DEFERRABLE INITIALLY DEFERRED -- if redirected
349 );
350
351 CREATE TABLE config.filter_dialog_interface (
352     key         TEXT                        PRIMARY KEY,
353     description TEXT
354 );
355
356 CREATE TABLE config.filter_dialog_filter_set (
357     id          SERIAL                      PRIMARY KEY,
358     name        TEXT                        NOT NULL,
359     owning_lib  INT                         NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
360     creator     INT                         NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
361     create_time TIMESTAMP WITH TIME ZONE    NOT NULL DEFAULT NOW(),
362     interface   TEXT                        NOT NULL REFERENCES config.filter_dialog_interface (key) DEFERRABLE INITIALLY DEFERRED,
363     filters     TEXT                        NOT NULL CHECK (is_json(filters)),
364     CONSTRAINT cfdfs_name_once_per_lib UNIQUE (name, owning_lib)
365 );
366  
367
368 SELECT evergreen.upgrade_deps_block_check('0753', :eg_version);
369
370 CREATE OR REPLACE FUNCTION url_verify.parse_url (url_in TEXT) RETURNS url_verify.url AS $$
371
372 use Rose::URI;
373
374 my $url_in = shift;
375 my $url = Rose::URI->new($url_in);
376
377 my %parts = map { $_ => $url->$_ } qw/scheme username password host port path query fragment/;
378
379 $parts{full_url} = $url_in;
380 ($parts{domain} = $parts{host}) =~ s/^[^.]+\.//;
381 ($parts{tld} = $parts{domain}) =~ s/(?:[^.]+\.)+//;
382 ($parts{page} = $parts{path}) =~ s#(?:[^/]*/)+##;
383
384 return \%parts;
385
386 $$ LANGUAGE PLPERLU;
387
388 CREATE OR REPLACE FUNCTION url_verify.ingest_url () RETURNS TRIGGER AS $$
389 DECLARE
390     tmp_row url_verify.url%ROWTYPE;
391 BEGIN
392     SELECT * INTO tmp_row FROM url_verify.parse_url(NEW.full_url);
393
394     NEW.scheme          := tmp_row.scheme;
395     NEW.username        := tmp_row.username;
396     NEW.password        := tmp_row.password;
397     NEW.host            := tmp_row.host;
398     NEW.domain          := tmp_row.domain;
399     NEW.tld             := tmp_row.tld;
400     NEW.port            := tmp_row.port;
401     NEW.path            := tmp_row.path;
402     NEW.page            := tmp_row.page;
403     NEW.query           := tmp_row.query;
404     NEW.fragment        := tmp_row.fragment;
405
406     RETURN NEW;
407 END;
408 $$ LANGUAGE PLPGSQL;
409
410 CREATE TRIGGER ingest_url_tgr
411     BEFORE INSERT ON url_verify.url
412     FOR EACH ROW EXECUTE PROCEDURE url_verify.ingest_url(); 
413
414 CREATE OR REPLACE FUNCTION url_verify.extract_urls ( session_id INT, item_id INT ) RETURNS INT AS $$
415 DECLARE
416     last_seen_tag TEXT;
417     current_tag TEXT;
418     current_sf TEXT;
419     current_url TEXT;
420     current_ord INT;
421     current_url_pos INT;
422     current_selector url_verify.url_selector%ROWTYPE;
423 BEGIN
424     current_ord := 1;
425
426     FOR current_selector IN SELECT * FROM url_verify.url_selector s WHERE s.session = session_id LOOP
427         current_url_pos := 1;
428         LOOP
429             SELECT  (XPATH(current_selector.xpath || '/text()', b.marc::XML))[current_url_pos]::TEXT INTO current_url
430               FROM  biblio.record_entry b
431                     JOIN container.biblio_record_entry_bucket_item c ON (c.target_biblio_record_entry = b.id)
432               WHERE c.id = item_id;
433
434             EXIT WHEN current_url IS NULL;
435
436             SELECT  (XPATH(current_selector.xpath || '/../@tag', b.marc::XML))[current_url_pos]::TEXT INTO current_tag
437               FROM  biblio.record_entry b
438                     JOIN container.biblio_record_entry_bucket_item c ON (c.target_biblio_record_entry = b.id)
439               WHERE c.id = item_id;
440
441             IF current_tag IS NULL THEN
442                 current_tag := last_seen_tag;
443             ELSE
444                 last_seen_tag := current_tag;
445             END IF;
446
447             SELECT  (XPATH(current_selector.xpath || '/@code', b.marc::XML))[current_url_pos]::TEXT INTO current_sf
448               FROM  biblio.record_entry b
449                     JOIN container.biblio_record_entry_bucket_item c ON (c.target_biblio_record_entry = b.id)
450               WHERE c.id = item_id;
451
452             INSERT INTO url_verify.url (session, item, url_selector, tag, subfield, ord, full_url)
453               VALUES ( session_id, item_id, current_selector.id, current_tag, current_sf, current_ord, current_url);
454
455             current_url_pos := current_url_pos + 1;
456             current_ord := current_ord + 1;
457         END LOOP;
458     END LOOP;
459
460     RETURN current_ord - 1;
461 END;
462 $$ LANGUAGE PLPGSQL;
463
464
465
466 -- NOTE: beware the use of bare perm IDs in the update_perm's below and in 
467 -- the 950 seed data file.  Update before merge to match current perm IDs! XXX
468
469
470 SELECT evergreen.upgrade_deps_block_check('0754', :eg_version);
471
472 INSERT INTO permission.perm_list (id, code, description) 
473     VALUES ( 
474         543, 
475         'URL_VERIFY',
476         oils_i18n_gettext(
477             543, 
478             'Allows a user to process and verify ULSs', 
479             'ppl', 
480             'description'
481         )
482     );
483
484
485 INSERT INTO permission.perm_list (id, code, description) 
486     VALUES ( 
487         544, 
488         'URL_VERIFY_UPDATE_SETTINGS',
489         oils_i18n_gettext(
490             544, 
491             'Allows a user to configure URL verification org unit settings',
492             'ppl', 
493             'description'
494         )
495     );
496
497
498 INSERT INTO permission.perm_list (id, code, description) 
499     VALUES ( 
500         545, 
501         'SAVED_FILTER_DIALOG_FILTERS',
502         oils_i18n_gettext(
503             545, 
504             'Allows users to save and load sets of filters for filter dialogs, available in certain staff interfaces',
505             'ppl', 
506             'description'
507         )
508     );
509
510
511 INSERT INTO config.settings_group (name, label)
512     VALUES (
513         'url_verify',
514         oils_i18n_gettext(
515             'url_verify',
516             'URL Verify',
517             'csg',
518             'label'
519         )
520     );
521
522 INSERT INTO config.org_unit_setting_type
523     (name, grp, label, description, datatype, update_perm)
524     VALUES (
525         'url_verify.url_verification_delay',
526         'url_verify',
527         oils_i18n_gettext(
528             'url_verify.url_verification_delay',
529             'Number of seconds to wait between URL test attempts.',
530             'coust',
531             'label'
532         ),
533         oils_i18n_gettext(
534             'url_verify.url_verification_delay',
535             'Throttling mechanism for batch URL verification runs.  Each running process will wait this number of seconds after a URL test before performing the next.',
536             'coust',
537             'description'
538         ),
539         'integer',
540         544
541     );
542
543 INSERT INTO config.org_unit_setting_type
544     (name, grp, label, description, datatype, update_perm)
545     VALUES (
546         'url_verify.url_verification_max_redirects',
547         'url_verify',
548         oils_i18n_gettext(
549             'url_verify.url_verification_max_redirects',
550             'Maximum redirect lookups',
551             'coust',
552             'label'
553         ),
554         oils_i18n_gettext(
555             'url_verify.url_verification_max_redirects',
556             'For URLs returning 3XX redirects, this is the maximum number of redirects we will follow before giving up.',
557             'coust',
558             'description'
559         ),
560         'integer',
561         544
562     );
563
564 INSERT INTO config.org_unit_setting_type
565     (name, grp, label, description, datatype, update_perm)
566     VALUES (
567         'url_verify.url_verification_max_wait',
568         'url_verify',
569         oils_i18n_gettext(
570             'url_verify.url_verification_max_wait',
571             'Maximum wait time (in seconds) for a URL to lookup',
572             'coust',
573             'label'
574         ),
575         oils_i18n_gettext(
576             'url_verify.url_verification_max_wait',
577             'If we exceed the wait time, the URL is marked as a "timeout" and the system moves on to the next URL',
578             'coust',
579             'description'
580         ),
581         'integer',
582         544
583     );
584
585
586 INSERT INTO config.org_unit_setting_type
587     (name, grp, label, description, datatype, update_perm)
588     VALUES (
589         'url_verify.verification_batch_size',
590         'url_verify',
591         oils_i18n_gettext(
592             'url_verify.verification_batch_size',
593             'Number of URLs to test in parallel',
594             'coust',
595             'label'
596         ),
597         oils_i18n_gettext(
598             'url_verify.verification_batch_size',
599             'URLs are tested in batches.  This number defines the size of each batch and it directly relates to the number of back-end processes performing URL verification.',
600             'coust',
601             'description'
602         ),
603         'integer',
604         544
605     );
606
607
608 INSERT INTO config.filter_dialog_interface (key, description) VALUES (
609     'url_verify',
610     oils_i18n_gettext(
611         'url_verify',
612         'All Link Checker filter dialogs',
613         'cfdi',
614         'description'
615     )
616 );
617
618
619 INSERT INTO config.usr_setting_type (name,grp,opac_visible,label,description,datatype) VALUES (
620     'ui.grid_columns.url_verify.select_urls',
621     'gui',
622     FALSE,
623     oils_i18n_gettext(
624         'ui.grid_columns.url_verify.select_urls',
625         'Link Checker''s URL Selection interface''s saved columns',
626         'cust',
627         'label'
628     ),
629     oils_i18n_gettext(
630         'ui.grid_columns.url_verify.select_urls',
631         'Link Checker''s URL Selection interface''s saved columns',
632         'cust',
633         'description'
634     ),
635     'string'
636 );
637
638 INSERT INTO config.usr_setting_type (name,grp,opac_visible,label,description,datatype) VALUES (
639     'ui.grid_columns.url_verify.review_attempt',
640     'gui',
641     FALSE,
642     oils_i18n_gettext(
643         'ui.grid_columns.url_verify.review_attempt',
644         'Link Checker''s Review Attempt interface''s saved columns',
645         'cust',
646         'label'
647     ),
648     oils_i18n_gettext(
649         'ui.grid_columns.url_verify.review_attempt',
650         'Link Checker''s Review Attempt interface''s saved columns',
651         'cust',
652         'description'
653     ),
654     'string'
655 );
656
657
658
659
660 SELECT evergreen.upgrade_deps_block_check('0755', :eg_version);
661
662 INSERT INTO config.org_unit_setting_type
663     (name, label, description, grp, datatype, fm_class) VALUES
664 (
665     'acq.upload.default.create_po',
666     oils_i18n_gettext(
667         'acq.upload.default.create_po',
668         'Upload Create PO',
669         'coust',
670         'label'
671     ),
672      oils_i18n_gettext(
673         'acq.upload.default.create_po',
674         'Create a purchase order by default during ACQ file upload',
675         'coust',
676         'description'
677     ),
678    'acq',
679     'bool',
680     NULL
681 ), (
682     'acq.upload.default.activate_po',
683     oils_i18n_gettext(
684         'acq.upload.default.activate_po',
685         'Upload Activate PO',
686         'coust',
687         'label'
688     ),
689      oils_i18n_gettext(
690         'acq.upload.default.activate_po',
691         'Activate the purchase order by default during ACQ file upload',
692         'coust',
693         'description'
694     ),
695     'acq',
696     'bool',
697     NULL
698 ), (
699     'acq.upload.default.provider',
700     oils_i18n_gettext(
701         'acq.upload.default.provider',
702         'Upload Default Provider',
703         'coust',
704         'label'
705     ),
706      oils_i18n_gettext(
707         'acq.upload.default.provider',
708         'Default provider to use during ACQ file upload',
709         'coust',
710         'description'
711     ),
712     'acq',
713     'link',
714     'acqpro'
715 ), (
716     'acq.upload.default.vandelay.match_set',
717     oils_i18n_gettext(
718         'acq.upload.default.vandelay.match_set',
719         'Upload Default Match Set',
720         'coust',
721         'label'
722     ),
723      oils_i18n_gettext(
724         'acq.upload.default.vandelay.match_set',
725         'Default match set to use during ACQ file upload',
726         'coust',
727         'description'
728     ),
729     'acq',
730     'link',
731     'vms'
732 ), (
733     'acq.upload.default.vandelay.merge_profile',
734     oils_i18n_gettext(
735         'acq.upload.default.vandelay.merge_profile',
736         'Upload Default Merge Profile',
737         'coust',
738         'label'
739     ),
740      oils_i18n_gettext(
741         'acq.upload.default.vandelay.merge_profile',
742         'Default merge profile to use during ACQ file upload',
743         'coust',
744         'description'
745     ),
746     'acq',
747     'link',
748     'vmp'
749 ), (
750     'acq.upload.default.vandelay.import_non_matching',
751     oils_i18n_gettext(
752         'acq.upload.default.vandelay.import_non_matching',
753         'Upload Import Non Matching by Default',
754         'coust',
755         'label'
756     ),
757      oils_i18n_gettext(
758         'acq.upload.default.vandelay.import_non_matching',
759         'Import non-matching records by default during ACQ file upload',
760         'coust',
761         'description'
762     ),
763     'acq',
764     'bool',
765     NULL
766 ), (
767     'acq.upload.default.vandelay.merge_on_exact',
768     oils_i18n_gettext(
769         'acq.upload.default.vandelay.merge_on_exact',
770         'Upload Merge on Exact Match by Default',
771         'coust',
772         'label'
773     ),
774      oils_i18n_gettext(
775         'acq.upload.default.vandelay.merge_on_exact',
776         'Merge records on exact match by default during ACQ file upload',
777         'coust',
778         'description'
779     ),
780     'acq',
781     'bool',
782     NULL
783 ), (
784     'acq.upload.default.vandelay.merge_on_best',
785     oils_i18n_gettext(
786         'acq.upload.default.vandelay.merge_on_best',
787         'Upload Merge on Best Match by Default',
788         'coust',
789         'label'
790     ),
791      oils_i18n_gettext(
792         'acq.upload.default.vandelay.merge_on_best',
793         'Merge records on best match by default during ACQ file upload',
794         'coust',
795         'description'
796     ),
797     'acq',
798     'bool',
799     NULL
800 ), (
801     'acq.upload.default.vandelay.merge_on_single',
802     oils_i18n_gettext(
803         'acq.upload.default.vandelay.merge_on_single',
804         'Upload Merge on Single Match by Default',
805         'coust',
806         'label'
807     ),
808      oils_i18n_gettext(
809         'acq.upload.default.vandelay.merge_on_single',
810         'Merge records on single match by default during ACQ file upload',
811         'coust',
812         'description'
813     ),
814     'acq',
815     'bool',
816     NULL
817 ), (
818     'acq.upload.default.vandelay.quality_ratio',
819     oils_i18n_gettext(
820         'acq.upload.default.vandelay.quality_ratio',
821         'Upload Default Min. Quality Ratio',
822         'coust',
823         'label'
824     ),
825      oils_i18n_gettext(
826         'acq.upload.default.vandelay.quality_ratio',
827         'Default minimum quality ratio used during ACQ file upload',
828         'coust',
829         'description'
830     ),
831     'acq',
832     'integer',
833     NULL
834 ), (
835     'acq.upload.default.vandelay.low_quality_fall_thru_profile',
836     oils_i18n_gettext(
837         'acq.upload.default.vandelay.low_quality_fall_thru_profile',
838         'Upload Default Insufficient Quality Fall-Thru Profile',
839         'coust',
840         'label'
841     ),
842      oils_i18n_gettext(
843         'acq.upload.default.vandelay.low_quality_fall_thru_profile',
844         'Default low-quality fall through profile used during ACQ file upload',
845         'coust',
846         'description'
847     ),
848     'acq',
849     'link',
850     'vmp'
851 ), (
852     'acq.upload.default.vandelay.load_item_for_imported',
853     oils_i18n_gettext(
854         'acq.upload.default.vandelay.load_item_for_imported',
855         'Upload Load Items for Imported Records by Default',
856         'coust',
857         'label'
858     ),
859      oils_i18n_gettext(
860         'acq.upload.default.vandelay.load_item_for_imported',
861         'Load items for imported records by default during ACQ file upload',
862         'coust',
863         'description'
864     ),
865     'acq',
866     'bool',
867     NULL
868 );
869
870
871 SELECT evergreen.upgrade_deps_block_check('0756', :eg_version);
872
873 -- Drop some lingering old functions in search schema
874 DROP FUNCTION IF EXISTS search.staged_fts(INT,INT,TEXT,INT[],INT[],TEXT[],TEXT[],TEXT[],TEXT[],TEXT[],TEXT[],TEXT[],TEXT,TEXT,TEXT,TEXT[],TEXT,REAL,TEXT,BOOL,BOOL,BOOL,INT,INT,INT);
875 DROP FUNCTION IF EXISTS search.parse_search_args(TEXT);
876 DROP FUNCTION IF EXISTS search.explode_array(ANYARRAY);
877 DROP FUNCTION IF EXISTS search.pick_table(TEXT);
878
879 -- Now drop query_parser_fts and related
880 DROP FUNCTION IF EXISTS search.query_parser_fts(INT,INT,TEXT,INT[],INT[],INT,INT,INT,BOOL,BOOL,INT);
881 DROP TYPE IF EXISTS search.search_result;
882 DROP TYPE IF EXISTS search.search_args;
883
884
885 SELECT evergreen.upgrade_deps_block_check('0757', :eg_version);
886
887 SET search_path = public, pg_catalog;
888
889 DO $$
890 DECLARE
891 lang TEXT;
892 BEGIN
893 FOR lang IN SELECT substring(pptsd.dictname from '(.*)_stem$') AS lang FROM pg_catalog.pg_ts_dict pptsd JOIN pg_catalog.pg_namespace ppn ON ppn.oid = pptsd.dictnamespace
894 WHERE ppn.nspname = 'pg_catalog' AND pptsd.dictname LIKE '%_stem' LOOP
895 RAISE NOTICE 'FOUND LANGUAGE %', lang;
896
897 EXECUTE 'DROP TEXT SEARCH DICTIONARY IF EXISTS ' || lang || '_nostop CASCADE;
898 CREATE TEXT SEARCH DICTIONARY ' || lang || '_nostop (TEMPLATE=pg_catalog.snowball, language=''' || lang || ''');
899 COMMENT ON TEXT SEARCH DICTIONARY ' || lang || '_nostop IS ''' ||lang || ' snowball stemmer with no stopwords for ASCII words only.'';
900 CREATE TEXT SEARCH CONFIGURATION ' || lang || '_nostop ( COPY = pg_catalog.' || lang || ' );
901 ALTER TEXT SEARCH CONFIGURATION ' || lang || '_nostop ALTER MAPPING FOR word, hword, hword_part WITH pg_catalog.simple;
902 ALTER TEXT SEARCH CONFIGURATION ' || lang || '_nostop ALTER MAPPING FOR asciiword, asciihword, hword_asciipart WITH ' || lang || '_nostop;';
903
904 END LOOP;
905 END;
906 $$;
907 CREATE TEXT SEARCH CONFIGURATION keyword ( COPY = english_nostop );
908 CREATE TEXT SEARCH CONFIGURATION "default" ( COPY = english_nostop );
909
910 SET search_path = evergreen, public, pg_catalog;
911
912 ALTER TABLE config.metabib_class
913     ADD COLUMN a_weight NUMERIC  DEFAULT 1.0 NOT NULL,
914     ADD COLUMN b_weight NUMERIC  DEFAULT 0.4 NOT NULL,
915     ADD COLUMN c_weight NUMERIC  DEFAULT 0.2 NOT NULL,
916     ADD COLUMN d_weight NUMERIC  DEFAULT 0.1 NOT NULL;
917
918 CREATE TABLE config.ts_config_list (
919     id      TEXT PRIMARY KEY,
920     name    TEXT NOT NULL
921 );
922 COMMENT ON TABLE config.ts_config_list IS $$
923 Full Text Configs
924
925 A list of full text configs with names and descriptions.
926 $$;
927
928 CREATE TABLE config.metabib_class_ts_map (
929     id              SERIAL PRIMARY KEY,
930     field_class     TEXT NOT NULL REFERENCES config.metabib_class (name),
931     ts_config       TEXT NOT NULL REFERENCES config.ts_config_list (id),
932     active          BOOL NOT NULL DEFAULT TRUE,
933     index_weight    CHAR(1) NOT NULL DEFAULT 'C' CHECK (index_weight IN ('A','B','C','D')),
934     index_lang      TEXT NULL,
935     search_lang     TEXT NULL,
936     always          BOOL NOT NULL DEFAULT true
937 );
938 COMMENT ON TABLE config.metabib_class_ts_map IS $$
939 Text Search Configs for metabib class indexing
940
941 This table contains text search config definitions for
942 storing index_vector values.
943 $$;
944
945 CREATE TABLE config.metabib_field_ts_map (
946     id              SERIAL PRIMARY KEY,
947     metabib_field   INT NOT NULL REFERENCES config.metabib_field (id),
948     ts_config       TEXT NOT NULL REFERENCES config.ts_config_list (id),
949     active          BOOL NOT NULL DEFAULT TRUE,
950     index_weight    CHAR(1) NOT NULL DEFAULT 'C' CHECK (index_weight IN ('A','B','C','D')),
951     index_lang      TEXT NULL,
952     search_lang     TEXT NULL
953 );
954 COMMENT ON TABLE config.metabib_field_ts_map IS $$
955 Text Search Configs for metabib field indexing
956
957 This table contains text search config definitions for
958 storing index_vector values.
959 $$;
960
961 CREATE TABLE metabib.combined_identifier_field_entry (
962     record          BIGINT      NOT NULL,
963     metabib_field   INT         NULL,
964     index_vector    tsvector    NOT NULL
965 );
966 CREATE UNIQUE INDEX metabib_combined_identifier_field_entry_fakepk_idx ON metabib.combined_identifier_field_entry (record, COALESCE(metabib_field::TEXT,''));
967 CREATE INDEX metabib_combined_identifier_field_entry_index_vector_idx ON metabib.combined_identifier_field_entry USING GIST (index_vector);
968 CREATE INDEX metabib_combined_identifier_field_source_idx ON metabib.combined_identifier_field_entry (metabib_field);
969
970 CREATE TABLE metabib.combined_title_field_entry (
971         record          BIGINT          NOT NULL,
972         metabib_field           INT             NULL,
973         index_vector    tsvector        NOT NULL
974 );
975 CREATE UNIQUE INDEX metabib_combined_title_field_entry_fakepk_idx ON metabib.combined_title_field_entry (record, COALESCE(metabib_field::TEXT,''));
976 CREATE INDEX metabib_combined_title_field_entry_index_vector_idx ON metabib.combined_title_field_entry USING GIST (index_vector);
977 CREATE INDEX metabib_combined_title_field_source_idx ON metabib.combined_title_field_entry (metabib_field);
978
979 CREATE TABLE metabib.combined_author_field_entry (
980         record          BIGINT          NOT NULL,
981         metabib_field           INT             NULL,
982         index_vector    tsvector        NOT NULL
983 );
984 CREATE UNIQUE INDEX metabib_combined_author_field_entry_fakepk_idx ON metabib.combined_author_field_entry (record, COALESCE(metabib_field::TEXT,''));
985 CREATE INDEX metabib_combined_author_field_entry_index_vector_idx ON metabib.combined_author_field_entry USING GIST (index_vector);
986 CREATE INDEX metabib_combined_author_field_source_idx ON metabib.combined_author_field_entry (metabib_field);
987
988 CREATE TABLE metabib.combined_subject_field_entry (
989         record          BIGINT          NOT NULL,
990         metabib_field           INT             NULL,
991         index_vector    tsvector        NOT NULL
992 );
993 CREATE UNIQUE INDEX metabib_combined_subject_field_entry_fakepk_idx ON metabib.combined_subject_field_entry (record, COALESCE(metabib_field::TEXT,''));
994 CREATE INDEX metabib_combined_subject_field_entry_index_vector_idx ON metabib.combined_subject_field_entry USING GIST (index_vector);
995 CREATE INDEX metabib_combined_subject_field_source_idx ON metabib.combined_subject_field_entry (metabib_field);
996
997 CREATE TABLE metabib.combined_keyword_field_entry (
998         record          BIGINT          NOT NULL,
999         metabib_field           INT             NULL,
1000         index_vector    tsvector        NOT NULL
1001 );
1002 CREATE UNIQUE INDEX metabib_combined_keyword_field_entry_fakepk_idx ON metabib.combined_keyword_field_entry (record, COALESCE(metabib_field::TEXT,''));
1003 CREATE INDEX metabib_combined_keyword_field_entry_index_vector_idx ON metabib.combined_keyword_field_entry USING GIST (index_vector);
1004 CREATE INDEX metabib_combined_keyword_field_source_idx ON metabib.combined_keyword_field_entry (metabib_field);
1005
1006 CREATE TABLE metabib.combined_series_field_entry (
1007         record          BIGINT          NOT NULL,
1008         metabib_field           INT             NULL,
1009         index_vector    tsvector        NOT NULL
1010 );
1011 CREATE UNIQUE INDEX metabib_combined_series_field_entry_fakepk_idx ON metabib.combined_series_field_entry (record, COALESCE(metabib_field::TEXT,''));
1012 CREATE INDEX metabib_combined_series_field_entry_index_vector_idx ON metabib.combined_series_field_entry USING GIST (index_vector);
1013 CREATE INDEX metabib_combined_series_field_source_idx ON metabib.combined_series_field_entry (metabib_field);
1014
1015 CREATE OR REPLACE FUNCTION metabib.update_combined_index_vectors(bib_id BIGINT) RETURNS VOID AS $func$
1016 BEGIN
1017     DELETE FROM metabib.combined_keyword_field_entry WHERE record = bib_id;
1018     INSERT INTO metabib.combined_keyword_field_entry(record, metabib_field, index_vector)
1019         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1020         FROM metabib.keyword_field_entry WHERE source = bib_id GROUP BY field;
1021     INSERT INTO metabib.combined_keyword_field_entry(record, metabib_field, index_vector)
1022         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1023         FROM metabib.keyword_field_entry WHERE source = bib_id;
1024
1025     DELETE FROM metabib.combined_title_field_entry WHERE record = bib_id;
1026     INSERT INTO metabib.combined_title_field_entry(record, metabib_field, index_vector)
1027         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1028         FROM metabib.title_field_entry WHERE source = bib_id GROUP BY field;
1029     INSERT INTO metabib.combined_title_field_entry(record, metabib_field, index_vector)
1030         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1031         FROM metabib.title_field_entry WHERE source = bib_id;
1032
1033     DELETE FROM metabib.combined_author_field_entry WHERE record = bib_id;
1034     INSERT INTO metabib.combined_author_field_entry(record, metabib_field, index_vector)
1035         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1036         FROM metabib.author_field_entry WHERE source = bib_id GROUP BY field;
1037     INSERT INTO metabib.combined_author_field_entry(record, metabib_field, index_vector)
1038         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1039         FROM metabib.author_field_entry WHERE source = bib_id;
1040
1041     DELETE FROM metabib.combined_subject_field_entry WHERE record = bib_id;
1042     INSERT INTO metabib.combined_subject_field_entry(record, metabib_field, index_vector)
1043         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1044         FROM metabib.subject_field_entry WHERE source = bib_id GROUP BY field;
1045     INSERT INTO metabib.combined_subject_field_entry(record, metabib_field, index_vector)
1046         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1047         FROM metabib.subject_field_entry WHERE source = bib_id;
1048
1049     DELETE FROM metabib.combined_series_field_entry WHERE record = bib_id;
1050     INSERT INTO metabib.combined_series_field_entry(record, metabib_field, index_vector)
1051         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1052         FROM metabib.series_field_entry WHERE source = bib_id GROUP BY field;
1053     INSERT INTO metabib.combined_series_field_entry(record, metabib_field, index_vector)
1054         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1055         FROM metabib.series_field_entry WHERE source = bib_id;
1056
1057     DELETE FROM metabib.combined_identifier_field_entry WHERE record = bib_id;
1058     INSERT INTO metabib.combined_identifier_field_entry(record, metabib_field, index_vector)
1059         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1060         FROM metabib.identifier_field_entry WHERE source = bib_id GROUP BY field;
1061     INSERT INTO metabib.combined_identifier_field_entry(record, metabib_field, index_vector)
1062         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1063         FROM metabib.identifier_field_entry WHERE source = bib_id;
1064
1065 END;
1066 $func$ LANGUAGE PLPGSQL;
1067
1068 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_field_entries( bib_id BIGINT, skip_facet BOOL DEFAULT FALSE, skip_browse BOOL DEFAULT FALSE, skip_search BOOL DEFAULT FALSE ) RETURNS VOID AS $func$
1069 DECLARE
1070     fclass          RECORD;
1071     ind_data        metabib.field_entry_template%ROWTYPE;
1072     mbe_row         metabib.browse_entry%ROWTYPE;
1073     mbe_id          BIGINT;
1074 BEGIN
1075     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
1076     IF NOT FOUND THEN
1077         IF NOT skip_search THEN
1078             FOR fclass IN SELECT * FROM config.metabib_class LOOP
1079                 -- RAISE NOTICE 'Emptying out %', fclass.name;
1080                 EXECUTE $$DELETE FROM metabib.$$ || fclass.name || $$_field_entry WHERE source = $$ || bib_id;
1081             END LOOP;
1082         END IF;
1083         IF NOT skip_facet THEN
1084             DELETE FROM metabib.facet_entry WHERE source = bib_id;
1085         END IF;
1086         IF NOT skip_browse THEN
1087             DELETE FROM metabib.browse_entry_def_map WHERE source = bib_id;
1088         END IF;
1089     END IF;
1090
1091     FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( bib_id ) LOOP
1092         IF ind_data.field < 0 THEN
1093             ind_data.field = -1 * ind_data.field;
1094         END IF;
1095
1096         IF ind_data.facet_field AND NOT skip_facet THEN
1097             INSERT INTO metabib.facet_entry (field, source, value)
1098                 VALUES (ind_data.field, ind_data.source, ind_data.value);
1099         END IF;
1100
1101         IF ind_data.browse_field AND NOT skip_browse THEN
1102             -- A caveat about this SELECT: this should take care of replacing
1103             -- old mbe rows when data changes, but not if normalization (by
1104             -- which I mean specifically the output of
1105             -- evergreen.oils_tsearch2()) changes.  It may or may not be
1106             -- expensive to add a comparison of index_vector to index_vector
1107             -- to the WHERE clause below.
1108             SELECT INTO mbe_row * FROM metabib.browse_entry WHERE value = ind_data.value;
1109             IF FOUND THEN
1110                 mbe_id := mbe_row.id;
1111             ELSE
1112                 INSERT INTO metabib.browse_entry (value) VALUES
1113                     (metabib.browse_normalize(ind_data.value, ind_data.field));
1114                 mbe_id := CURRVAL('metabib.browse_entry_id_seq'::REGCLASS);
1115             END IF;
1116
1117             INSERT INTO metabib.browse_entry_def_map (entry, def, source)
1118                 VALUES (mbe_id, ind_data.field, ind_data.source);
1119         END IF;
1120
1121         IF ind_data.search_field AND NOT skip_search THEN
1122             EXECUTE $$
1123                 INSERT INTO metabib.$$ || ind_data.field_class || $$_field_entry (field, source, value)
1124                     VALUES ($$ ||
1125                         quote_literal(ind_data.field) || $$, $$ ||
1126                         quote_literal(ind_data.source) || $$, $$ ||
1127                         quote_literal(ind_data.value) ||
1128                     $$);$$;
1129         END IF;
1130
1131     END LOOP;
1132
1133     IF NOT skip_search THEN
1134         PERFORM metabib.update_combined_index_vectors(bib_id);
1135     END IF;
1136
1137     RETURN;
1138 END;
1139 $func$ LANGUAGE PLPGSQL;
1140
1141 DROP FUNCTION IF EXISTS evergreen.oils_tsearch2() CASCADE;
1142 DROP FUNCTION IF EXISTS public.oils_tsearch2() CASCADE;
1143
1144 CREATE OR REPLACE FUNCTION public.oils_tsearch2 () RETURNS TRIGGER AS $$
1145 DECLARE
1146     normalizer      RECORD;
1147     value           TEXT := '';
1148     temp_vector     TEXT := '';
1149     ts_rec          RECORD;
1150     cur_weight      "char";
1151 BEGIN
1152     value := NEW.value;
1153     NEW.index_vector = ''::tsvector;
1154
1155     IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
1156         FOR normalizer IN
1157             SELECT  n.func AS func,
1158                     n.param_count AS param_count,
1159                     m.params AS params
1160               FROM  config.index_normalizer n
1161                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
1162               WHERE field = NEW.field
1163               ORDER BY m.pos LOOP
1164                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
1165                     quote_literal( value ) ||
1166                     CASE
1167                         WHEN normalizer.param_count > 0
1168                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1169                             ELSE ''
1170                         END ||
1171                     ')' INTO value;
1172
1173         END LOOP;
1174         NEW.value = value;
1175     END IF;
1176
1177     IF TG_TABLE_NAME::TEXT ~ 'browse_entry$' THEN
1178         value :=  ARRAY_TO_STRING(
1179             evergreen.regexp_split_to_array(value, E'\\W+'), ' '
1180         );
1181         value := public.search_normalize(value);
1182         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
1183     ELSIF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
1184         FOR ts_rec IN
1185             SELECT ts_config, index_weight
1186             FROM config.metabib_class_ts_map
1187             WHERE field_class = TG_ARGV[0]
1188                 AND index_lang IS NULL OR EXISTS (SELECT 1 FROM metabib.record_attr WHERE id = NEW.source AND index_lang IN(attrs->'item_lang',attrs->'language'))
1189                 AND always OR NOT EXISTS (SELECT 1 FROM config.metabib_field_ts_map WHERE metabib_field = NEW.field)
1190             UNION
1191             SELECT ts_config, index_weight
1192             FROM config.metabib_field_ts_map
1193             WHERE metabib_field = NEW.field
1194                AND index_lang IS NULL OR EXISTS (SELECT 1 FROM metabib.record_attr WHERE id = NEW.source AND index_lang IN(attrs->'item_lang',attrs->'language'))
1195             ORDER BY index_weight ASC
1196         LOOP
1197             IF cur_weight IS NOT NULL AND cur_weight != ts_rec.index_weight THEN
1198                 NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
1199                 temp_vector = '';
1200             END IF;
1201             cur_weight = ts_rec.index_weight;
1202             SELECT INTO temp_vector temp_vector || ' ' || to_tsvector(ts_rec.ts_config::regconfig, value)::TEXT;
1203         END LOOP;
1204         NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
1205     ELSE
1206         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
1207     END IF;
1208
1209     RETURN NEW;
1210 END;
1211 $$ LANGUAGE PLPGSQL;
1212
1213 CREATE TRIGGER authority_full_rec_fti_trigger
1214     BEFORE UPDATE OR INSERT ON authority.full_rec
1215     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
1216
1217 CREATE TRIGGER authority_simple_heading_fti_trigger
1218     BEFORE UPDATE OR INSERT ON authority.simple_heading
1219     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
1220
1221 CREATE TRIGGER metabib_identifier_field_entry_fti_trigger
1222     BEFORE UPDATE OR INSERT ON metabib.identifier_field_entry
1223     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('identifier');
1224
1225 CREATE TRIGGER metabib_title_field_entry_fti_trigger
1226     BEFORE UPDATE OR INSERT ON metabib.title_field_entry
1227     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('title');
1228
1229 CREATE TRIGGER metabib_author_field_entry_fti_trigger
1230     BEFORE UPDATE OR INSERT ON metabib.author_field_entry
1231     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('author');
1232
1233 CREATE TRIGGER metabib_subject_field_entry_fti_trigger
1234     BEFORE UPDATE OR INSERT ON metabib.subject_field_entry
1235     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('subject');
1236
1237 CREATE TRIGGER metabib_keyword_field_entry_fti_trigger
1238     BEFORE UPDATE OR INSERT ON metabib.keyword_field_entry
1239     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
1240
1241 CREATE TRIGGER metabib_series_field_entry_fti_trigger
1242     BEFORE UPDATE OR INSERT ON metabib.series_field_entry
1243     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('series');
1244
1245 CREATE TRIGGER metabib_browse_entry_fti_trigger
1246     BEFORE INSERT OR UPDATE ON metabib.browse_entry
1247     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
1248
1249 CREATE TRIGGER metabib_full_rec_fti_trigger
1250     BEFORE UPDATE OR INSERT ON metabib.real_full_rec
1251     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('default');
1252
1253 INSERT INTO config.ts_config_list(id, name) VALUES
1254     ('simple','Non-Stemmed Simple'),
1255     ('danish_nostop','Danish Stemmed'),
1256     ('dutch_nostop','Dutch Stemmed'),
1257     ('english_nostop','English Stemmed'),
1258     ('finnish_nostop','Finnish Stemmed'),
1259     ('french_nostop','French Stemmed'),
1260     ('german_nostop','German Stemmed'),
1261     ('hungarian_nostop','Hungarian Stemmed'),
1262     ('italian_nostop','Italian Stemmed'),
1263     ('norwegian_nostop','Norwegian Stemmed'),
1264     ('portuguese_nostop','Portuguese Stemmed'),
1265     ('romanian_nostop','Romanian Stemmed'),
1266     ('russian_nostop','Russian Stemmed'),
1267     ('spanish_nostop','Spanish Stemmed'),
1268     ('swedish_nostop','Swedish Stemmed'),
1269     ('turkish_nostop','Turkish Stemmed');
1270
1271 INSERT INTO config.metabib_class_ts_map(field_class, ts_config, index_weight, always) VALUES
1272     ('keyword','simple','A',true),
1273     ('keyword','english_nostop','C',true),
1274     ('title','simple','A',true),
1275     ('title','english_nostop','C',true),
1276     ('author','simple','A',true),
1277     ('author','english_nostop','C',true),
1278     ('series','simple','A',true),
1279     ('series','english_nostop','C',true),
1280     ('subject','simple','A',true),
1281     ('subject','english_nostop','C',true),
1282     ('identifier','simple','A',true);
1283
1284 CREATE OR REPLACE FUNCTION evergreen.rel_bump(terms TEXT[], value TEXT, bumps TEXT[], mults NUMERIC[]) RETURNS NUMERIC AS
1285 $BODY$
1286 use strict;
1287 my ($terms,$value,$bumps,$mults) = @_;
1288
1289 my $retval = 1;
1290
1291 for (my $id = 0; $id < @$bumps; $id++) {
1292         if ($bumps->[$id] eq 'first_word') {
1293                 $retval *= $mults->[$id] if ($value =~ /^$terms->[0]/);
1294         } elsif ($bumps->[$id] eq 'full_match') {
1295                 my $fullmatch = join(' ', @$terms);
1296                 $retval *= $mults->[$id] if ($value =~ /^$fullmatch$/);
1297         } elsif ($bumps->[$id] eq 'word_order') {
1298                 my $wordorder = join('.*', @$terms);
1299                 $retval *= $mults->[$id] if ($value =~ /$wordorder/);
1300         }
1301 }
1302 return $retval;
1303 $BODY$ LANGUAGE plperlu IMMUTABLE STRICT COST 100;
1304
1305 /* ** This happens in the supplemental script **
1306
1307 UPDATE metabib.identifier_field_entry set value = value;
1308 UPDATE metabib.title_field_entry set value = value;
1309 UPDATE metabib.author_field_entry set value = value;
1310 UPDATE metabib.subject_field_entry set value = value;
1311 UPDATE metabib.keyword_field_entry set value = value;
1312 UPDATE metabib.series_field_entry set value = value;
1313
1314 SELECT metabib.update_combined_index_vectors(id)
1315     FROM biblio.record_entry
1316     WHERE NOT deleted;
1317
1318 */
1319
1320 SELECT evergreen.upgrade_deps_block_check('0758', :eg_version);
1321
1322 INSERT INTO config.settings_group (name, label) VALUES
1323     ('vandelay', 'Vandelay');
1324
1325 INSERT INTO config.org_unit_setting_type (name, grp, label, datatype, fm_class) VALUES
1326     ('vandelay.default_match_set', 'vandelay', 'Default Record Match Set', 'link', 'vms');
1327
1328
1329 SELECT evergreen.upgrade_deps_block_check('0759', :eg_version);
1330
1331 CREATE TABLE actor.org_unit_proximity_adjustment (
1332     id                  SERIAL   PRIMARY KEY,
1333     item_circ_lib       INT         REFERENCES actor.org_unit (id),
1334     item_owning_lib     INT         REFERENCES actor.org_unit (id),
1335     copy_location       INT         REFERENCES asset.copy_location (id),
1336     hold_pickup_lib     INT         REFERENCES actor.org_unit (id),
1337     hold_request_lib    INT         REFERENCES actor.org_unit (id),
1338     pos                 INT         NOT NULL DEFAULT 0,
1339     absolute_adjustment BOOL        NOT NULL DEFAULT FALSE,
1340     prox_adjustment     NUMERIC,
1341     circ_mod            TEXT,       -- REFERENCES config.circ_modifier (code),
1342     CONSTRAINT prox_adj_criterium CHECK (COALESCE(item_circ_lib::TEXT,item_owning_lib::TEXT,copy_location::TEXT,hold_pickup_lib::TEXT,hold_request_lib::TEXT,circ_mod) IS NOT NULL)
1343 );
1344 CREATE UNIQUE INDEX prox_adj_once_idx ON actor.org_unit_proximity_adjustment (item_circ_lib,item_owning_lib,copy_location,hold_pickup_lib,hold_request_lib,circ_mod);
1345 CREATE INDEX prox_adj_circ_lib_idx ON actor.org_unit_proximity_adjustment (item_circ_lib);
1346 CREATE INDEX prox_adj_owning_lib_idx ON actor.org_unit_proximity_adjustment (item_owning_lib);
1347 CREATE INDEX prox_adj_copy_location_idx ON actor.org_unit_proximity_adjustment (copy_location);
1348 CREATE INDEX prox_adj_pickup_lib_idx ON actor.org_unit_proximity_adjustment (hold_pickup_lib);
1349 CREATE INDEX prox_adj_request_lib_idx ON actor.org_unit_proximity_adjustment (hold_request_lib);
1350 CREATE INDEX prox_adj_circ_mod_idx ON actor.org_unit_proximity_adjustment (circ_mod);
1351
1352 CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
1353     WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
1354             SELECT $1, 0
1355         UNION
1356             SELECT ou.parent_ou, ouad.distance+1
1357             FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON (ou.id = ouad.id)
1358             WHERE ou.parent_ou IS NOT NULL
1359     )
1360     SELECT * FROM org_unit_ancestors_distance;
1361 $$ LANGUAGE SQL STABLE ROWS 1;
1362
1363 CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(
1364     ahr_id INT,
1365     acp_id BIGINT,
1366     copy_context_ou INT DEFAULT NULL
1367     -- TODO maybe? hold_context_ou INT DEFAULT NULL.  This would optionally
1368     -- support an "ahprox" measurement: adjust prox between copy circ lib and
1369     -- hold request lib, but I'm unsure whether to use this theoretical
1370     -- argument only in the baseline calculation or later in the other
1371     -- queries in this function.
1372 ) RETURNS NUMERIC AS $f$
1373 DECLARE
1374     aoupa           actor.org_unit_proximity_adjustment%ROWTYPE;
1375     ahr             action.hold_request%ROWTYPE;
1376     acp             asset.copy%ROWTYPE;
1377     acn             asset.call_number%ROWTYPE;
1378     acl             asset.copy_location%ROWTYPE;
1379     baseline_prox   NUMERIC;
1380
1381     icl_list        INT[];
1382     iol_list        INT[];
1383     isl_list        INT[];
1384     hpl_list        INT[];
1385     hrl_list        INT[];
1386
1387 BEGIN
1388
1389     SELECT * INTO ahr FROM action.hold_request WHERE id = ahr_id;
1390     SELECT * INTO acp FROM asset.copy WHERE id = acp_id;
1391     SELECT * INTO acn FROM asset.call_number WHERE id = acp.call_number;
1392     SELECT * INTO acl FROM asset.copy_location WHERE id = acp.location;
1393
1394     IF copy_context_ou IS NULL THEN
1395         copy_context_ou := acp.circ_lib;
1396     END IF;
1397
1398     -- First, gather the baseline proximity of "here" to pickup lib
1399     SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = copy_context_ou AND to_org = ahr.pickup_lib;
1400
1401     -- Find any absolute adjustments, and set the baseline prox to that
1402     SELECT  adj.* INTO aoupa
1403       FROM  actor.org_unit_proximity_adjustment adj
1404             LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
1405             LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
1406             LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
1407             LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
1408             LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
1409       WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
1410         absolute_adjustment AND
1411         COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
1412       ORDER BY
1413             COALESCE(acp_cl.distance,999)
1414                 + COALESCE(acn_ol.distance,999)
1415                 + COALESCE(acl_ol.distance,999)
1416                 + COALESCE(ahr_pl.distance,999)
1417                 + COALESCE(ahr_rl.distance,999),
1418             adj.pos
1419       LIMIT 1;
1420
1421     IF FOUND THEN
1422         baseline_prox := aoupa.prox_adjustment;
1423     END IF;
1424
1425     -- Now find any relative adjustments, and change the baseline prox based on them
1426     FOR aoupa IN
1427         SELECT  adj.* 
1428           FROM  actor.org_unit_proximity_adjustment adj
1429                 LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
1430                 LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
1431                 LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
1432                 LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
1433                 LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
1434           WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
1435             NOT absolute_adjustment AND
1436             COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
1437     LOOP
1438         baseline_prox := baseline_prox + aoupa.prox_adjustment;
1439     END LOOP;
1440
1441     RETURN baseline_prox;
1442 END;
1443 $f$ LANGUAGE PLPGSQL;
1444
1445 ALTER TABLE actor.org_unit_proximity_adjustment
1446     ADD CONSTRAINT actor_org_unit_proximity_adjustment_circ_mod_fkey
1447     FOREIGN KEY (circ_mod) REFERENCES config.circ_modifier (code)
1448     DEFERRABLE INITIALLY DEFERRED;
1449
1450 ALTER TABLE action.hold_copy_map ADD COLUMN proximity NUMERIC;
1451
1452
1453 SELECT evergreen.upgrade_deps_block_check('0760', :eg_version);
1454
1455 CREATE TABLE config.best_hold_order(
1456     id          SERIAL      PRIMARY KEY,    -- (metadata)
1457     name        TEXT        UNIQUE,   -- i18n (metadata)
1458     pprox       INT, -- copy capture <-> pickup lib prox
1459     hprox       INT, -- copy circ lib <-> request lib prox
1460     aprox       INT, -- copy circ lib <-> pickup lib ADJUSTED prox on ahcm
1461     approx      INT, -- copy capture <-> pickup lib ADJUSTED prox from function
1462     priority    INT, -- group hold priority
1463     cut         INT, -- cut-in-line
1464     depth       INT, -- selection depth
1465     htime       INT, -- time since last home-lib circ exceeds org-unit setting
1466     rtime       INT, -- request time
1467     shtime      INT  -- time since copy last trip home exceeds org-unit setting
1468 );
1469
1470 -- At least one of these columns must contain a non-null value
1471 ALTER TABLE config.best_hold_order ADD CHECK ((
1472     pprox IS NOT NULL OR
1473     hprox IS NOT NULL OR
1474     aprox IS NOT NULL OR
1475     priority IS NOT NULL OR
1476     cut IS NOT NULL OR
1477     depth IS NOT NULL OR
1478     htime IS NOT NULL OR
1479     rtime IS NOT NULL
1480 ));
1481
1482 INSERT INTO config.best_hold_order (
1483     name,
1484     pprox, aprox, priority, cut, depth, rtime, htime, hprox
1485 ) VALUES (
1486     'Traditional',
1487     1, 2, 3, 4, 5, 6, 7, 8
1488 );
1489
1490 INSERT INTO config.best_hold_order (
1491     name,
1492     hprox, pprox, aprox, priority, cut, depth, rtime, htime
1493 ) VALUES (
1494     'Traditional with Holds-always-go-home',
1495     1, 2, 3, 4, 5, 6, 7, 8
1496 );
1497
1498 INSERT INTO config.best_hold_order (
1499     name,
1500     htime, hprox, pprox, aprox, priority, cut, depth, rtime
1501 ) VALUES (
1502     'Traditional with Holds-go-home',
1503     1, 2, 3, 4, 5, 6, 7, 8
1504 );
1505
1506 INSERT INTO config.best_hold_order (
1507     name,
1508     priority, cut, rtime, depth, pprox, hprox, aprox, htime
1509 ) VALUES (
1510     'FIFO',
1511     1, 2, 3, 4, 5, 6, 7, 8
1512 );
1513
1514 INSERT INTO config.best_hold_order (
1515     name,
1516     hprox, priority, cut, rtime, depth, pprox, aprox, htime
1517 ) VALUES (
1518     'FIFO with Holds-always-go-home',
1519     1, 2, 3, 4, 5, 6, 7, 8
1520 );
1521
1522 INSERT INTO config.best_hold_order (
1523     name,
1524     htime, priority, cut, rtime, depth, pprox, aprox, hprox
1525 ) VALUES (
1526     'FIFO with Holds-go-home',
1527     1, 2, 3, 4, 5, 6, 7, 8
1528 );
1529
1530 INSERT INTO permission.perm_list (
1531     id, code, description
1532 ) VALUES (
1533     546,
1534     'ADMIN_HOLD_CAPTURE_SORT',
1535     oils_i18n_gettext(
1536         546,
1537         'Allows a user to make changes to best-hold selection sort order',
1538         'ppl',
1539         'description'
1540     )
1541 );
1542
1543 INSERT INTO config.org_unit_setting_type (
1544     name, label, description, datatype, fm_class, update_perm, grp
1545 ) VALUES (
1546     'circ.hold_capture_order',
1547     oils_i18n_gettext(
1548         'circ.hold_capture_order',
1549         'Best-hold selection sort order',
1550         'coust',
1551         'label'
1552     ),
1553     oils_i18n_gettext(
1554         'circ.hold_capture_order',
1555         'Defines the sort order of holds when selecting a hold to fill using a given copy at capture time',
1556         'coust',
1557         'description'
1558     ),
1559     'link',
1560     'cbho',
1561     546,
1562     'holds'
1563 );
1564
1565 INSERT INTO config.org_unit_setting_type (
1566     name, label, description, datatype, update_perm, grp
1567 ) VALUES (
1568     'circ.hold_go_home_interval',
1569     oils_i18n_gettext(
1570         'circ.hold_go_home_interval',
1571         'Max foreign-circulation time',
1572         'coust',
1573         'label'
1574     ),
1575     oils_i18n_gettext(
1576         'circ.hold_go_home_interval',
1577         'Time a copy can spend circulating away from its circ lib before returning there to fill a hold (if one exists there)',
1578         'coust',
1579         'description'
1580     ),
1581     'interval',
1582     546,
1583     'holds'
1584 );
1585
1586 INSERT INTO actor.org_unit_setting (
1587     org_unit, name, value
1588 ) VALUES (
1589     (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL),
1590     'circ.hold_go_home_interval',
1591     '"6 months"'
1592 );
1593
1594 UPDATE actor.org_unit_setting SET
1595     name = 'circ.hold_capture_order',
1596     value = (SELECT id FROM config.best_hold_order WHERE name = 'FIFO')
1597 WHERE
1598     name = 'circ.holds_fifo' AND value ILIKE '%true%';
1599
1600
1601 SELECT evergreen.upgrade_deps_block_check('0762', :eg_version);
1602
1603 INSERT INTO config.internal_flag (name) VALUES ('ingest.skip_browse_indexing');
1604 INSERT INTO config.internal_flag (name) VALUES ('ingest.skip_search_indexing');
1605 INSERT INTO config.internal_flag (name) VALUES ('ingest.skip_facet_indexing');
1606
1607 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_field_entries( bib_id BIGINT, skip_facet BOOL DEFAULT FALSE, skip_browse BOOL DEFAULT FALSE, skip_search BOOL DEFAULT FALSE ) RETURNS VOID AS $func$
1608 DECLARE
1609     fclass          RECORD;
1610     ind_data        metabib.field_entry_template%ROWTYPE;
1611     mbe_row         metabib.browse_entry%ROWTYPE;
1612     mbe_id          BIGINT;
1613     b_skip_facet    BOOL;
1614     b_skip_browse   BOOL;
1615     b_skip_search   BOOL;
1616 BEGIN
1617
1618     SELECT COALESCE(NULLIF(skip_facet, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_facet_indexing' AND enabled)) INTO b_skip_facet;
1619     SELECT COALESCE(NULLIF(skip_browse, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_browse_indexing' AND enabled)) INTO b_skip_browse;
1620     SELECT COALESCE(NULLIF(skip_search, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_search_indexing' AND enabled)) INTO b_skip_search;
1621
1622     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
1623     IF NOT FOUND THEN
1624         IF NOT b_skip_search THEN
1625             FOR fclass IN SELECT * FROM config.metabib_class LOOP
1626                 -- RAISE NOTICE 'Emptying out %', fclass.name;
1627                 EXECUTE $$DELETE FROM metabib.$$ || fclass.name || $$_field_entry WHERE source = $$ || bib_id;
1628             END LOOP;
1629         END IF;
1630         IF NOT b_skip_facet THEN
1631             DELETE FROM metabib.facet_entry WHERE source = bib_id;
1632         END IF;
1633         IF NOT b_skip_browse THEN
1634             DELETE FROM metabib.browse_entry_def_map WHERE source = bib_id;
1635         END IF;
1636     END IF;
1637
1638     FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( bib_id ) LOOP
1639         IF ind_data.field < 0 THEN
1640             ind_data.field = -1 * ind_data.field;
1641         END IF;
1642
1643         IF ind_data.facet_field AND NOT b_skip_facet THEN
1644             INSERT INTO metabib.facet_entry (field, source, value)
1645                 VALUES (ind_data.field, ind_data.source, ind_data.value);
1646         END IF;
1647
1648         IF ind_data.browse_field AND NOT b_skip_browse THEN
1649             -- A caveat about this SELECT: this should take care of replacing
1650             -- old mbe rows when data changes, but not if normalization (by
1651             -- which I mean specifically the output of
1652             -- evergreen.oils_tsearch2()) changes.  It may or may not be
1653             -- expensive to add a comparison of index_vector to index_vector
1654             -- to the WHERE clause below.
1655             SELECT INTO mbe_row * FROM metabib.browse_entry WHERE value = ind_data.value;
1656             IF FOUND THEN
1657                 mbe_id := mbe_row.id;
1658             ELSE
1659                 INSERT INTO metabib.browse_entry (value) VALUES
1660                     (metabib.browse_normalize(ind_data.value, ind_data.field));
1661                 mbe_id := CURRVAL('metabib.browse_entry_id_seq'::REGCLASS);
1662             END IF;
1663
1664             INSERT INTO metabib.browse_entry_def_map (entry, def, source)
1665                 VALUES (mbe_id, ind_data.field, ind_data.source);
1666         END IF;
1667
1668         IF ind_data.search_field AND NOT b_skip_search THEN
1669             EXECUTE $$
1670                 INSERT INTO metabib.$$ || ind_data.field_class || $$_field_entry (field, source, value)
1671                     VALUES ($$ ||
1672                         quote_literal(ind_data.field) || $$, $$ ||
1673                         quote_literal(ind_data.source) || $$, $$ ||
1674                         quote_literal(ind_data.value) ||
1675                     $$);$$;
1676         END IF;
1677
1678     END LOOP;
1679
1680     IF NOT b_skip_search THEN
1681         PERFORM metabib.update_combined_index_vectors(bib_id);
1682     END IF;
1683
1684     RETURN;
1685 END;
1686 $func$ LANGUAGE PLPGSQL;
1687
1688
1689 SELECT evergreen.upgrade_deps_block_check('0763', :eg_version);
1690
1691 INSERT INTO config.org_unit_setting_type (
1692     name, label, grp, datatype
1693 ) VALUES (
1694     'circ.fines.truncate_to_max_fine',
1695     'Truncate fines to max fine amount',
1696     'circ',
1697     'bool'
1698 );
1699
1700
1701
1702 SELECT evergreen.upgrade_deps_block_check('0765', :eg_version);
1703
1704 ALTER TABLE acq.provider
1705     ADD COLUMN default_copy_count INTEGER NOT NULL DEFAULT 0;
1706
1707
1708 SELECT evergreen.upgrade_deps_block_check('0768', :eg_version);
1709
1710 CREATE OR REPLACE FUNCTION evergreen.rank_ou(lib INT, search_lib INT, pref_lib INT DEFAULT NULL)
1711 RETURNS INTEGER AS $$
1712     SELECT COALESCE(
1713
1714         -- lib matches search_lib
1715         (SELECT CASE WHEN $1 = $2 THEN -20000 END),
1716
1717         -- lib matches pref_lib
1718         (SELECT CASE WHEN $1 = $3 THEN -10000 END),
1719
1720
1721         -- pref_lib is a child of search_lib and lib is a child of pref lib.  
1722         (SELECT distance - 5000
1723             FROM actor.org_unit_descendants_distance($3) 
1724             WHERE id = $1 AND $3 IN (
1725                 SELECT id FROM actor.org_unit_descendants($2))),
1726
1727         -- lib is a child of search_lib
1728         (SELECT distance FROM actor.org_unit_descendants_distance($2) WHERE id = $1),
1729
1730         -- all others pay cash
1731         1000
1732     );
1733 $$ LANGUAGE SQL STABLE;
1734
1735
1736
1737
1738 SELECT evergreen.upgrade_deps_block_check('0769', :eg_version);
1739
1740 DROP FUNCTION IF EXISTS 
1741     evergreen.ranked_volumes(BIGINT, INT, INT, HSTORE, HSTORE, INT);
1742
1743 CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
1744     bibid BIGINT, 
1745     ouid INT,
1746     depth INT DEFAULT NULL,
1747     slimit HSTORE DEFAULT NULL,
1748     soffset HSTORE DEFAULT NULL,
1749     pref_lib INT DEFAULT NULL,
1750     includes TEXT[] DEFAULT NULL::TEXT[]
1751 ) RETURNS TABLE (id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT) AS $$
1752     SELECT ua.id, ua.name, ua.label_sortkey, MIN(ua.rank) AS rank FROM (
1753         SELECT acn.id, aou.name, acn.label_sortkey,
1754             evergreen.rank_ou(aou.id, $2, $6), evergreen.rank_cp_status(acp.status),
1755             RANK() OVER w
1756         FROM asset.call_number acn
1757             JOIN asset.copy acp ON (acn.id = acp.call_number)
1758             JOIN actor.org_unit_descendants( $2, COALESCE(
1759                 $3, (
1760                     SELECT depth
1761                     FROM actor.org_unit_type aout
1762                         INNER JOIN actor.org_unit ou ON ou_type = aout.id
1763                     WHERE ou.id = $2
1764                 ), $6)
1765             ) AS aou ON (acp.circ_lib = aou.id)
1766         WHERE acn.record = $1
1767             AND acn.deleted IS FALSE
1768             AND acp.deleted IS FALSE
1769             AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN 
1770                 EXISTS (
1771                     SELECT 1 
1772                     FROM asset.opac_visible_copies 
1773                     WHERE copy_id = acp.id AND record = acn.record
1774                 ) ELSE TRUE END
1775         GROUP BY acn.id, acp.status, aou.name, acn.label_sortkey, aou.id
1776         WINDOW w AS (
1777             ORDER BY evergreen.rank_ou(aou.id, $2, $6), evergreen.rank_cp_status(acp.status)
1778         )
1779     ) AS ua
1780     GROUP BY ua.id, ua.name, ua.label_sortkey
1781     ORDER BY rank, ua.name, ua.label_sortkey
1782     LIMIT ($4 -> 'acn')::INT
1783     OFFSET ($5 -> 'acn')::INT;
1784 $$
1785 LANGUAGE SQL STABLE;
1786
1787 CREATE OR REPLACE FUNCTION unapi.holdings_xml (
1788     bid BIGINT,
1789     ouid INT,
1790     org TEXT,
1791     depth INT DEFAULT NULL,
1792     includes TEXT[] DEFAULT NULL::TEXT[],
1793     slimit HSTORE DEFAULT NULL,
1794     soffset HSTORE DEFAULT NULL,
1795     include_xmlns BOOL DEFAULT TRUE,
1796     pref_lib INT DEFAULT NULL
1797 )
1798 RETURNS XML AS $F$
1799      SELECT  XMLELEMENT(
1800                  name holdings,
1801                  XMLATTRIBUTES(
1802                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
1803                     CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id,
1804                     (SELECT record_has_holdable_copy FROM asset.record_has_holdable_copy($1)) AS has_holdable
1805                  ),
1806                  XMLELEMENT(
1807                      name counts,
1808                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
1809                          SELECT  XMLELEMENT(
1810                                      name count,
1811                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
1812                                  )::text
1813                            FROM  asset.opac_ou_record_copy_count($2,  $1)
1814                                      UNION
1815                          SELECT  XMLELEMENT(
1816                                      name count,
1817                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
1818                                  )::text
1819                            FROM  asset.staff_ou_record_copy_count($2, $1)
1820                                      UNION
1821                          SELECT  XMLELEMENT(
1822                                      name count,
1823                                      XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
1824                                  )::text
1825                            FROM  asset.opac_ou_record_copy_count($9,  $1)
1826                                      ORDER BY 1
1827                      )x)
1828                  ),
1829                  CASE 
1830                      WHEN ('bmp' = ANY ($5)) THEN
1831                         XMLELEMENT(
1832                             name monograph_parts,
1833                             (SELECT XMLAGG(bmp) FROM (
1834                                 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)
1835                                   FROM  biblio.monograph_part
1836                                   WHERE record = $1
1837                             )x)
1838                         )
1839                      ELSE NULL
1840                  END,
1841                  XMLELEMENT(
1842                      name volumes,
1843                      (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
1844                         -- Physical copies
1845                         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
1846                         FROM evergreen.ranked_volumes($1, $2, $4, $6, $7, $9, $5) AS y
1847                         UNION ALL
1848                         -- Located URIs
1849                         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), 0, name, label_sortkey
1850                         FROM evergreen.located_uris($1, $2, $9) AS uris
1851                      )x)
1852                  ),
1853                  CASE WHEN ('ssub' = ANY ($5)) THEN 
1854                      XMLELEMENT(
1855                          name subscriptions,
1856                          (SELECT XMLAGG(ssub) FROM (
1857                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
1858                               FROM  serial.subscription
1859                               WHERE record_entry = $1
1860                         )x)
1861                      )
1862                  ELSE NULL END,
1863                  CASE WHEN ('acp' = ANY ($5)) THEN 
1864                      XMLELEMENT(
1865                          name foreign_copies,
1866                          (SELECT XMLAGG(acp) FROM (
1867                             SELECT  unapi.acp(p.target_copy,'xml','copy',evergreen.array_remove_item_by_value($5,'acp'), $3, $4, $6, $7, FALSE)
1868                               FROM  biblio.peer_bib_copy_map p
1869                                     JOIN asset.copy c ON (p.target_copy = c.id)
1870                               WHERE NOT c.deleted AND p.peer_record = $1
1871                             LIMIT ($6 -> 'acp')::INT
1872                             OFFSET ($7 -> 'acp')::INT
1873                         )x)
1874                      )
1875                  ELSE NULL END
1876              );
1877 $F$ LANGUAGE SQL STABLE;
1878
1879
1880
1881 SELECT evergreen.upgrade_deps_block_check('0771', :eg_version);
1882
1883 INSERT INTO action_trigger.hook (
1884         key,
1885         core_type,
1886         description,
1887         passive
1888     ) VALUES (
1889         'au.barred',
1890         'au',
1891         'A user was barred by staff',
1892         FALSE
1893     );
1894
1895 INSERT INTO action_trigger.hook (
1896         key,
1897         core_type,
1898         description,
1899         passive
1900     ) VALUES (
1901         'au.unbarred',
1902         'au',
1903         'A user was un-barred by staff',
1904         FALSE
1905     );
1906
1907 INSERT INTO action_trigger.validator (
1908         module, 
1909         description
1910     ) VALUES (
1911         'PatronBarred',
1912         'Tests if a patron is currently marked as barred'
1913     );
1914
1915 INSERT INTO action_trigger.validator (
1916         module, 
1917         description
1918     ) VALUES (
1919         'PatronNotBarred',
1920         'Tests if a patron is currently not marked as barred'
1921     );
1922
1923
1924 SELECT evergreen.upgrade_deps_block_check('0772', :eg_version);
1925
1926 INSERT INTO config.internal_flag (name) VALUES ('ingest.metarecord_mapping.preserve_on_delete');  -- defaults to false/off
1927
1928 DROP RULE protect_bib_rec_delete ON biblio.record_entry;
1929 CREATE RULE protect_bib_rec_delete AS
1930     ON DELETE TO biblio.record_entry DO INSTEAD (
1931         UPDATE biblio.record_entry
1932             SET deleted = TRUE
1933             WHERE OLD.id = biblio.record_entry.id
1934     );
1935
1936
1937 -- AFTER UPDATE OR INSERT trigger for biblio.record_entry
1938 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
1939 DECLARE
1940     transformed_xml TEXT;
1941     prev_xfrm       TEXT;
1942     normalizer      RECORD;
1943     xfrm            config.xml_transform%ROWTYPE;
1944     attr_value      TEXT;
1945     new_attrs       HSTORE := ''::HSTORE;
1946     attr_def        config.record_attr_definition%ROWTYPE;
1947 BEGIN
1948
1949     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
1950         PERFORM * FROM config.internal_flag WHERE
1951             name = 'ingest.metarecord_mapping.preserve_on_delete' AND enabled;
1952         IF NOT FOUND THEN
1953             -- One needs to keep these around to support searches
1954             -- with the #deleted modifier, so one should turn on the named
1955             -- internal flag for that functionality.
1956             DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id;
1957             DELETE FROM metabib.record_attr WHERE id = NEW.id;
1958         END IF;
1959
1960         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
1961         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
1962         DELETE FROM metabib.browse_entry_def_map WHERE source = NEW.id; -- Don't auto-suggest deleted bibs
1963         RETURN NEW; -- and we're done
1964     END IF;
1965
1966     IF TG_OP = 'UPDATE' THEN -- re-ingest?
1967         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
1968
1969         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
1970             RETURN NEW;
1971         END IF;
1972     END IF;
1973
1974     -- Record authority linking
1975     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
1976     IF NOT FOUND THEN
1977         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
1978     END IF;
1979
1980     -- Flatten and insert the mfr data
1981     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
1982     IF NOT FOUND THEN
1983         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
1984
1985         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
1986         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
1987         IF NOT FOUND THEN
1988             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
1989
1990                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1991                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
1992                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
1993                       WHERE record = NEW.id
1994                             AND tag LIKE attr_def.tag
1995                             AND CASE
1996                                 WHEN attr_def.sf_list IS NOT NULL 
1997                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
1998                                 ELSE TRUE
1999                                 END
2000                       GROUP BY tag
2001                       ORDER BY tag
2002                       LIMIT 1;
2003
2004                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
2005                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
2006
2007                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
2008
2009                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
2010             
2011                     -- See if we can skip the XSLT ... it's expensive
2012                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
2013                         -- Can't skip the transform
2014                         IF xfrm.xslt <> '---' THEN
2015                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
2016                         ELSE
2017                             transformed_xml := NEW.marc;
2018                         END IF;
2019             
2020                         prev_xfrm := xfrm.name;
2021                     END IF;
2022
2023                     IF xfrm.name IS NULL THEN
2024                         -- just grab the marcxml (empty) transform
2025                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
2026                         prev_xfrm := xfrm.name;
2027                     END IF;
2028
2029                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
2030
2031                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
2032                     SELECT  m.value INTO attr_value
2033                       FROM  biblio.marc21_physical_characteristics(NEW.id) v
2034                             JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
2035                       WHERE v.subfield = attr_def.phys_char_sf
2036                       LIMIT 1; -- Just in case ...
2037
2038                 END IF;
2039
2040                 -- apply index normalizers to attr_value
2041                 FOR normalizer IN
2042                     SELECT  n.func AS func,
2043                             n.param_count AS param_count,
2044                             m.params AS params
2045                       FROM  config.index_normalizer n
2046                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
2047                       WHERE attr = attr_def.name
2048                       ORDER BY m.pos LOOP
2049                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
2050                             COALESCE( quote_literal( attr_value ), 'NULL' ) ||
2051                             CASE
2052                                 WHEN normalizer.param_count > 0
2053                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
2054                                     ELSE ''
2055                                 END ||
2056                             ')' INTO attr_value;
2057         
2058                 END LOOP;
2059
2060                 -- Add the new value to the hstore
2061                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
2062
2063             END LOOP;
2064
2065             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
2066                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
2067             ELSE
2068                 UPDATE metabib.record_attr SET attrs = new_attrs WHERE id = NEW.id;
2069             END IF;
2070
2071         END IF;
2072     END IF;
2073
2074     -- Gather and insert the field entry data
2075     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
2076
2077     -- Located URI magic
2078     IF TG_OP = 'INSERT' THEN
2079         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
2080         IF NOT FOUND THEN
2081             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
2082         END IF;
2083     ELSE
2084         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
2085         IF NOT FOUND THEN
2086             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
2087         END IF;
2088     END IF;
2089
2090     -- (re)map metarecord-bib linking
2091     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
2092         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
2093         IF NOT FOUND THEN
2094             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
2095         END IF;
2096     ELSE -- we're doing an update, and we're not deleted, remap
2097         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
2098         IF NOT FOUND THEN
2099             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
2100         END IF;
2101     END IF;
2102
2103     RETURN NEW;
2104 END;
2105 $func$ LANGUAGE PLPGSQL;
2106
2107
2108 -- Evergreen DB patch xxxx.data.authority_thesaurus_update.sql
2109 --
2110
2111 -- check whether patch can be applied
2112 SELECT evergreen.upgrade_deps_block_check('0773', :eg_version);
2113
2114
2115 INSERT INTO authority.thesaurus (code, name, control_set) VALUES
2116     (' ', oils_i18n_gettext(' ','Alternate no attempt to code','at','name'), NULL);
2117
2118
2119
2120 SELECT evergreen.upgrade_deps_block_check('0774', :eg_version);
2121
2122 CREATE TABLE config.z3950_source_credentials (
2123     id SERIAL PRIMARY KEY,
2124     owner INTEGER NOT NULL REFERENCES actor.org_unit(id),
2125     source TEXT NOT NULL REFERENCES config.z3950_source(name) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
2126     -- do some Z servers require a username but no password or vice versa?
2127     username TEXT,
2128     password TEXT,
2129     CONSTRAINT czsc_source_once_per_lib UNIQUE (source, owner)
2130 );
2131
2132 -- find the most relevant set of credentials for the Z source and org
2133 CREATE OR REPLACE FUNCTION config.z3950_source_credentials_lookup
2134         (source TEXT, owner INTEGER) 
2135         RETURNS config.z3950_source_credentials AS $$
2136
2137     SELECT creds.* 
2138     FROM config.z3950_source_credentials creds
2139         JOIN actor.org_unit aou ON (aou.id = creds.owner)
2140         JOIN actor.org_unit_type aout ON (aout.id = aou.ou_type)
2141     WHERE creds.source = $1 AND creds.owner IN ( 
2142         SELECT id FROM actor.org_unit_ancestors($2) 
2143     )
2144     ORDER BY aout.depth DESC LIMIT 1;
2145
2146 $$ LANGUAGE SQL STABLE;
2147
2148 -- since we are not exposing config.z3950_source_credentials
2149 -- via the IDL, providing a stored proc gives us a way to
2150 -- set values in the table via cstore
2151 CREATE OR REPLACE FUNCTION config.z3950_source_credentials_apply
2152         (src TEXT, org INTEGER, uname TEXT, passwd TEXT) 
2153         RETURNS VOID AS $$
2154 BEGIN
2155     PERFORM 1 FROM config.z3950_source_credentials
2156         WHERE owner = org AND source = src;
2157
2158     IF FOUND THEN
2159         IF COALESCE(uname, '') = '' AND COALESCE(passwd, '') = '' THEN
2160             DELETE FROM config.z3950_source_credentials 
2161                 WHERE owner = org AND source = src;
2162         ELSE 
2163             UPDATE config.z3950_source_credentials 
2164                 SET username = uname, password = passwd
2165                 WHERE owner = org AND source = src;
2166         END IF;
2167     ELSE
2168         IF COALESCE(uname, '') <> '' OR COALESCE(passwd, '') <> '' THEN
2169             INSERT INTO config.z3950_source_credentials
2170                 (source, owner, username, password) 
2171                 VALUES (src, org, uname, passwd);
2172         END IF;
2173     END IF;
2174 END;
2175 $$ LANGUAGE PLPGSQL;
2176
2177
2178
2179
2180 SELECT evergreen.upgrade_deps_block_check('0775', :eg_version);
2181
2182 ALTER TABLE config.z3950_attr
2183     DROP CONSTRAINT z3950_attr_source_fkey,
2184     ADD CONSTRAINT z3950_attr_source_fkey 
2185         FOREIGN KEY (source) 
2186         REFERENCES config.z3950_source(name) 
2187         ON UPDATE CASCADE
2188         ON DELETE CASCADE
2189         DEFERRABLE INITIALLY DEFERRED;
2190
2191
2192 SELECT evergreen.upgrade_deps_block_check('0776', :eg_version);
2193
2194 ALTER TABLE acq.lineitem_attr
2195     ADD COLUMN order_ident BOOLEAN NOT NULL DEFAULT FALSE;
2196
2197 INSERT INTO permission.perm_list ( id, code, description ) VALUES (
2198     547, -- VERIFY
2199     'ACQ_ADD_LINEITEM_IDENTIFIER',
2200     oils_i18n_gettext(
2201         547,-- VERIFY
2202         'When granted, newly added lineitem identifiers will propagate to linked bib records',
2203         'ppl',
2204         'description'
2205     )
2206 );
2207
2208 INSERT INTO permission.perm_list ( id, code, description ) VALUES (
2209     548, -- VERIFY
2210     'ACQ_SET_LINEITEM_IDENTIFIER',
2211     oils_i18n_gettext(
2212         548,-- VERIFY
2213         'Allows staff to change the lineitem identifier',
2214         'ppl',
2215         'description'
2216     )
2217 );
2218
2219
2220 SELECT evergreen.upgrade_deps_block_check('0777', :eg_version);
2221
2222 -- Listed here for reference / ease of access.  The update
2223 -- is not applied here (see the WHERE clause).
2224 UPDATE action_trigger.event_definition SET template = 
2225 $$
2226 [%- USE date -%]
2227 [%
2228     # extract some commonly used variables
2229
2230     VENDOR_SAN = target.provider.san;
2231     VENDCODE = target.provider.edi_default.vendcode;
2232     VENDACCT = target.provider.edi_default.vendacct;
2233     ORG_UNIT_SAN = target.ordering_agency.mailing_address.san;
2234
2235     # set the vendor / provider
2236
2237     VENDOR_BT      = 0; # Baker & Taylor
2238     VENDOR_INGRAM  = 0;
2239     VENDOR_BRODART = 0;
2240     VENDOR_MW_TAPE = 0; # Midwest Tape
2241     VENDOR_RB      = 0; # Recorded Books
2242     VENDOR_ULS     = 0; # ULS
2243
2244     IF    VENDOR_SAN == '1556150'; VENDOR_BT = 1;
2245     ELSIF VENDOR_SAN == '1697684'; VENDOR_BRODART = 1;
2246     ELSIF VENDOR_SAN == '1697978'; VENDOR_INGRAM = 1;
2247     ELSIF VENDOR_SAN == '2549913'; VENDOR_MW_TAPE = 1;
2248     ELSIF VENDOR_SAN == '1113984'; VENDOR_RB = 1;
2249     ELSIF VENDOR_SAN == '1699342'; VENDOR_ULS = 1;
2250     END;
2251
2252     # if true, pass the PO name as a secondary identifier
2253     # RFF+LI:<name>/li_id
2254     INC_PO_NAME = 0;
2255     IF VENDOR_INGRAM;
2256         INC_PO_NAME = 1;
2257     END;
2258
2259     # GIR configuration --------------------------------------
2260
2261     INC_COPIES = 1; # copies on/off switch
2262     INC_FUND = 0;
2263     INC_CALLNUMBER = 0;
2264     INC_ITEM_TYPE = 1;
2265     INC_LOCATION = 0;
2266     INC_COLLECTION_CODE = 1;
2267     INC_OWNING_LIB = 1;
2268     INC_QUANTITY = 1;
2269     INC_COPY_ID = 0;
2270
2271     IF VENDOR_BT;
2272         INC_CALLNUMBER = 1;
2273     END;
2274
2275     IF VENDOR_BRODART;
2276         INC_FUND = 1;
2277     END;
2278
2279     IF VENDOR_MW_TAPE;
2280         INC_FUND = 1;
2281         INC_COLLECTION_CODE = 0;
2282         INC_ITEM_TYPE = 0;
2283     END;
2284
2285     # END GIR configuration ---------------------------------
2286
2287 -%]
2288 [%- BLOCK big_block -%]
2289 {
2290    "recipient":"[% VENDOR_SAN %]",
2291    "sender":"[% ORG_UNIT_SAN %]",
2292    "body": [{
2293      "ORDERS":[ "order", {
2294
2295         "po_number":[% target.id %],
2296
2297         [% IF INC_PO_NAME %]
2298         "po_name":"[% target.name | replace('\/', ' ') | replace('"', '\"') %]",
2299         [% END %]
2300
2301         "date":"[% date.format(date.now, '%Y%m%d') %]",
2302
2303         "buyer":[
2304             [% IF VENDOR_BT %]
2305                 {"id-qualifier": 91, "id":"[% ORG_UNIT_SAN %] [% VENDCODE %]"}
2306             [% ELSE %]
2307                 {"id":"[% ORG_UNIT_SAN %]"},
2308                 {"id-qualifier": 91, "id":"[% VENDACCT %]"}
2309             [% END %]
2310         ],
2311
2312         "vendor":[
2313             "[% VENDOR_SAN %]",
2314             {"id-qualifier": 92, "id":"[% target.provider.id %]"}
2315         ],
2316
2317         "currency":"[% target.provider.currency_type %]",
2318                 
2319         "items":[
2320         [%- FOR li IN target.lineitems %]
2321         {
2322             "line_index":"[% li.id %]",
2323             "identifiers":[   
2324             [%- 
2325                 idval = '';
2326                 idqual = 'EN'; # default ISBN/UPC/EAN-13
2327                 ident_attr = helpers.get_li_order_ident(li.attributes);
2328                 IF ident_attr;
2329                     idname = ident_attr.attr_name;
2330                     idval = ident_attr.attr_value;
2331                     IF idname == 'isbn' AND idval.length != 13;
2332                         idqual = 'IB';
2333                     ELSIF idname == 'issn';
2334                         idqual = 'IS';
2335                     END;
2336                 ELSE;
2337                     idqual = 'IN';
2338                     idval = li.id;
2339                 END -%]
2340                 {"id-qualifier":"[% idqual %]","id":"[% idval %]"}
2341             ],
2342             "price":[% li.estimated_unit_price || '0.00' %],
2343             "desc":[
2344                 {"BTI":"[% helpers.get_li_attr_jedi('title',     '', li.attributes) %]"},
2345                 {"BPU":"[% helpers.get_li_attr_jedi('publisher', '', li.attributes) %]"},
2346                 {"BPD":"[% helpers.get_li_attr_jedi('pubdate',   '', li.attributes) %]"},
2347                 [% IF VENDOR_ULS -%]
2348                 {"BEN":"[% helpers.get_li_attr_jedi('edition',   '', li.attributes) %]"},
2349                 {"BAU":"[% helpers.get_li_attr_jedi('author',    '', li.attributes) %]"}
2350                 [%- ELSE -%]
2351                 {"BPH":"[% helpers.get_li_attr_jedi('pagination','', li.attributes) %]"}
2352                 [%- END %]
2353             ],
2354             [%- ftx_vals = []; 
2355                 FOR note IN li.lineitem_notes;
2356                     NEXT UNLESS note.vendor_public == 't'; 
2357                     ftx_vals.push(note.value); 
2358                 END; 
2359                 IF VENDOR_BRODART; # look for copy-level spec code
2360                     FOR lid IN li.lineitem_details;
2361                         IF lid.note;
2362                             spec_note = lid.note.match('spec code ([a-zA-Z0-9_])');
2363                             IF spec_note.0; ftx_vals.push(spec_note.0); END;
2364                         END;
2365                     END;
2366                 END; 
2367                 IF xtra_ftx;           ftx_vals.unshift(xtra_ftx); END; 
2368
2369                 # BT & ULS want FTX+LIN for every LI, even if empty
2370                 IF ((VENDOR_BT OR VENDOR_ULS) AND ftx_vals.size == 0);
2371                     ftx_vals.unshift('');
2372                 END;  
2373             -%]
2374
2375             "free-text":[ 
2376                 [% FOR note IN ftx_vals -%] "[% note %]"[% UNLESS loop.last %], [% END %][% END %] 
2377             ],            
2378
2379             "quantity":[% li.lineitem_details.size %],
2380
2381             [%- IF INC_COPIES -%]
2382             "copies" : [
2383                 [%- compressed_copies = [];
2384                     FOR lid IN li.lineitem_details;
2385                         fund = lid.fund.code;
2386                         item_type = lid.circ_modifier;
2387                         callnumber = lid.cn_label;
2388                         owning_lib = lid.owning_lib.shortname;
2389                         location = lid.location;
2390                         collection_code = lid.collection_code;
2391     
2392                         # when we have real copy data, treat it as authoritative for some fields
2393                         acp = lid.eg_copy_id;
2394                         IF acp;
2395                             item_type = acp.circ_modifier;
2396                             callnumber = acp.call_number.label;
2397                             location = acp.location.name;
2398                         END ;
2399
2400
2401                         # collapse like copies into groups w/ quantity
2402
2403                         found_match = 0;
2404                         IF !INC_COPY_ID; # INC_COPY_ID implies 1 copy per GIR
2405                             FOR copy IN compressed_copies;
2406                                 IF  (fund == copy.fund OR (!fund AND !copy.fund)) AND
2407                                     (item_type == copy.item_type OR (!item_type AND !copy.item_type)) AND
2408                                     (callnumber == copy.callnumber OR (!callnumber AND !copy.callnumber)) AND
2409                                     (owning_lib == copy.owning_lib OR (!owning_lib AND !copy.owning_lib)) AND
2410                                     (location == copy.location OR (!location AND !copy.location)) AND
2411                                     (collection_code == copy.collection_code OR (!collection_code AND !copy.collection_code));
2412
2413                                     copy.quantity = copy.quantity + 1;
2414                                     found_match = 1;
2415                                 END;
2416                             END;
2417                         END;
2418
2419                         IF !found_match;
2420                             compressed_copies.push({
2421                                 fund => fund,
2422                                 item_type => item_type,
2423                                 callnumber => callnumber,
2424                                 owning_lib => owning_lib,
2425                                 location => location,
2426                                 collection_code => collection_code,
2427                                 copy_id => lid.id, # for INC_COPY_ID
2428                                 quantity => 1
2429                             });
2430                         END;
2431                     END;
2432                     FOR copy IN compressed_copies;
2433
2434                     # If we assume owning_lib is required and set, 
2435                     # it is safe to prepend each following copy field w/ a ","
2436
2437                     # B&T EDI requires expected GIR fields to be 
2438                     # present regardless of whether a value exists.  
2439                     # some fields are required to have a value in ACQ, 
2440                     # though, so they are not forced into place below.
2441
2442                  %]{[%- IF INC_OWNING_LIB AND copy.owning_lib %] "owning_lib":"[% copy.owning_lib %]"[% END -%]
2443                     [%- IF INC_FUND AND copy.fund %],"fund":"[% copy.fund %]"[% END -%]
2444                     [%- IF INC_CALLNUMBER AND (VENDOR_BT OR copy.callnumber) %],"call_number":"[% copy.callnumber %]"[% END -%]
2445                     [%- IF INC_ITEM_TYPE AND (VENDOR_BT OR copy.item_type) %],"item_type":"[% copy.item_type %]"[% END -%]
2446                     [%- IF INC_LOCATION AND copy.location %],"copy_location":"[% copy.location %]"[% END -%]
2447                     [%- IF INC_COLLECTION_CODE AND (VENDOR_BT OR copy.collection_code) %],"collection_code":"[% copy.collection_code %]"[% END -%]
2448                     [%- IF INC_QUANTITY %],"quantity":"[% copy.quantity %]"[% END -%]
2449                     [%- IF INC_COPY_ID %],"copy_id":"[% copy.copy_id %]" [% END %]}[% ',' UNLESS loop.last -%]
2450                 [%- END -%] [%# FOR compressed_copies -%]
2451             ]
2452             [%- END -%] [%# IF INC_COPIES %]
2453
2454         }[% UNLESS loop.last %],[% END -%]
2455
2456         [% END %] [%# END lineitems %]
2457         ],
2458         "line_items":[% target.lineitems.size %]
2459      }]  [%# close ORDERS array %]
2460    }]    [%# close  body  array %]
2461 }
2462 [% END %]
2463 [% tempo = PROCESS big_block; helpers.escape_json(tempo) %]
2464 $$
2465 WHERE ID = 23 AND FALSE; -- remove 'AND FALSE' to apply this update
2466
2467
2468 -- lineitem worksheet
2469 UPDATE action_trigger.event_definition SET template = 
2470 $$
2471 [%- USE date -%]
2472 [%-
2473     # find a lineitem attribute by name and optional type
2474     BLOCK get_li_attr;
2475         FOR attr IN li.attributes;
2476             IF attr.attr_name == attr_name;
2477                 IF !attr_type OR attr_type == attr.attr_type;
2478                     attr.attr_value;
2479                     LAST;
2480                 END;
2481             END;
2482         END;
2483     END
2484 -%]
2485
2486 <h2>Purchase Order [% target.id %]</h2>
2487 <br/>
2488 date <b>[% date.format(date.now, '%Y%m%d') %]</b>
2489 <br/>
2490
2491 <style>
2492     table td { padding:5px; border:1px solid #aaa;}
2493     table { width:95%; border-collapse:collapse; }
2494     #vendor-notes { padding:5px; border:1px solid #aaa; }
2495 </style>
2496 <table id='vendor-table'>
2497   <tr>
2498     <td valign='top'>Vendor</td>
2499     <td>
2500       <div>[% target.provider.name %]</div>
2501       <div>[% target.provider.addresses.0.street1 %]</div>
2502       <div>[% target.provider.addresses.0.street2 %]</div>
2503       <div>[% target.provider.addresses.0.city %]</div>
2504       <div>[% target.provider.addresses.0.state %]</div>
2505       <div>[% target.provider.addresses.0.country %]</div>
2506       <div>[% target.provider.addresses.0.post_code %]</div>
2507     </td>
2508     <td valign='top'>Ship to / Bill to</td>
2509     <td>
2510       <div>[% target.ordering_agency.name %]</div>
2511       <div>[% target.ordering_agency.billing_address.street1 %]</div>
2512       <div>[% target.ordering_agency.billing_address.street2 %]</div>
2513       <div>[% target.ordering_agency.billing_address.city %]</div>
2514       <div>[% target.ordering_agency.billing_address.state %]</div>
2515       <div>[% target.ordering_agency.billing_address.country %]</div>
2516       <div>[% target.ordering_agency.billing_address.post_code %]</div>
2517     </td>
2518   </tr>
2519 </table>
2520
2521 <br/><br/>
2522 <fieldset id='vendor-notes'>
2523     <legend>Notes to the Vendor</legend>
2524     <ul>
2525     [% FOR note IN target.notes %]
2526         [% IF note.vendor_public == 't' %]
2527             <li>[% note.value %]</li>
2528         [% END %]
2529     [% END %]
2530     </ul>
2531 </fieldset>
2532 <br/><br/>
2533
2534 <table>
2535   <thead>
2536     <tr>
2537       <th>PO#</th>
2538       <th>ISBN or Item #</th>
2539       <th>Title</th>
2540       <th>Quantity</th>
2541       <th>Unit Price</th>
2542       <th>Line Total</th>
2543       <th>Notes</th>
2544     </tr>
2545   </thead>
2546   <tbody>
2547
2548   [% subtotal = 0 %]
2549   [% FOR li IN target.lineitems %]
2550
2551   <tr>
2552     [% count = li.lineitem_details.size %]
2553     [% price = li.estimated_unit_price %]
2554     [% litotal = (price * count) %]
2555     [% subtotal = subtotal + litotal %]
2556     [% 
2557         ident_attr = helpers.get_li_order_ident(li.attributes);
2558         SET ident_value = ident_attr.attr_value IF ident_attr;
2559     %]
2560     <td>[% target.id %]</td>
2561     <td>[% ident_value %]</td>
2562     <td>[% PROCESS get_li_attr attr_name = 'title' %]</td>
2563     <td>[% count %]</td>
2564     <td>[% price %]</td>
2565     <td>[% litotal %]</td>
2566     <td>
2567         <ul>
2568         [% FOR note IN li.lineitem_notes %]
2569             [% IF note.vendor_public == 't' %]
2570                 <li>[% note.value %]</li>
2571             [% END %]
2572         [% END %]
2573         </ul>
2574     </td>
2575   </tr>
2576   [% END %]
2577   <tr>
2578     <td/><td/><td/><td/>
2579     <td>Subtotal</td>
2580     <td>[% subtotal %]</td>
2581   </tr>
2582   </tbody>
2583 </table>
2584
2585 <br/>
2586
2587 Total Line Item Count: [% target.lineitems.size %]
2588 $$
2589 WHERE ID = 4; -- PO HTML
2590
2591
2592 SELECT evergreen.upgrade_deps_block_check('0778', :eg_version);
2593
2594 CREATE OR REPLACE FUNCTION extract_marc_field_set
2595         (TEXT, BIGINT, TEXT, TEXT) RETURNS SETOF TEXT AS $$
2596 DECLARE
2597     query TEXT;
2598     output TEXT;
2599 BEGIN
2600     FOR output IN
2601         SELECT x.t FROM (
2602             SELECT id,t
2603                 FROM  oils_xpath_table(
2604                     'id', 'marc', $1, $3, 'id = ' || $2)
2605                 AS t(id int, t text))x
2606         LOOP
2607         IF $4 IS NOT NULL THEN
2608             SELECT INTO output (SELECT regexp_replace(output, $4, '', 'g'));
2609         END IF;
2610         RETURN NEXT output;
2611     END LOOP;
2612     RETURN;
2613 END;
2614 $$ LANGUAGE PLPGSQL IMMUTABLE;
2615
2616
2617 CREATE OR REPLACE FUNCTION 
2618         public.extract_acq_marc_field_set ( BIGINT, TEXT, TEXT) 
2619         RETURNS SETOF TEXT AS $$
2620         SELECT extract_marc_field_set('acq.lineitem', $1, $2, $3);
2621 $$ LANGUAGE SQL;
2622
2623
2624 CREATE OR REPLACE FUNCTION public.ingest_acq_marc ( ) RETURNS TRIGGER AS $function$
2625 DECLARE
2626         value           TEXT;
2627         atype           TEXT;
2628         prov            INT;
2629         pos             INT;
2630         adef            RECORD;
2631         xpath_string    TEXT;
2632 BEGIN
2633         FOR adef IN SELECT *,tableoid FROM acq.lineitem_attr_definition LOOP
2634
2635                 SELECT relname::TEXT INTO atype FROM pg_class WHERE oid = adef.tableoid;
2636
2637                 IF (atype NOT IN ('lineitem_usr_attr_definition','lineitem_local_attr_definition')) THEN
2638                         IF (atype = 'lineitem_provider_attr_definition') THEN
2639                                 SELECT provider INTO prov FROM acq.lineitem_provider_attr_definition WHERE id = adef.id;
2640                                 CONTINUE WHEN NEW.provider IS NULL OR prov <> NEW.provider;
2641                         END IF;
2642                         
2643                         IF (atype = 'lineitem_provider_attr_definition') THEN
2644                                 SELECT xpath INTO xpath_string FROM acq.lineitem_provider_attr_definition WHERE id = adef.id;
2645                         ELSIF (atype = 'lineitem_marc_attr_definition') THEN
2646                                 SELECT xpath INTO xpath_string FROM acq.lineitem_marc_attr_definition WHERE id = adef.id;
2647                         ELSIF (atype = 'lineitem_generated_attr_definition') THEN
2648                                 SELECT xpath INTO xpath_string FROM acq.lineitem_generated_attr_definition WHERE id = adef.id;
2649                         END IF;
2650
2651             xpath_string := REGEXP_REPLACE(xpath_string,$re$//?text\(\)$$re$,'');
2652
2653             IF (adef.code = 'title' OR adef.code = 'author') THEN
2654                 -- title and author should not be split
2655                 -- FIXME: once oils_xpath can grok XPATH 2.0 functions, we can use
2656                 -- string-join in the xpath and remove this special case
2657                         SELECT extract_acq_marc_field(id, xpath_string, adef.remove) INTO value FROM acq.lineitem WHERE id = NEW.id;
2658                         IF (value IS NOT NULL AND value <> '') THEN
2659                                     INSERT INTO acq.lineitem_attr (lineitem, definition, attr_type, attr_name, attr_value)
2660                                     VALUES (NEW.id, adef.id, atype, adef.code, value);
2661                 END IF;
2662             ELSE
2663                 pos := 1;
2664                 LOOP
2665                     -- each application of the regex may produce multiple values
2666                     FOR value IN
2667                         SELECT * FROM extract_acq_marc_field_set(
2668                             NEW.id, xpath_string || '[' || pos || ']', adef.remove)
2669                         LOOP
2670
2671                         IF (value IS NOT NULL AND value <> '') THEN
2672                             INSERT INTO acq.lineitem_attr
2673                                 (lineitem, definition, attr_type, attr_name, attr_value)
2674                                 VALUES (NEW.id, adef.id, atype, adef.code, value);
2675                         ELSE
2676                             EXIT;
2677                         END IF;
2678                     END LOOP;
2679                     IF NOT FOUND THEN
2680                         EXIT;
2681                     END IF;
2682                     pos := pos + 1;
2683                END LOOP;
2684             END IF;
2685
2686                 END IF;
2687
2688         END LOOP;
2689
2690         RETURN NULL;
2691 END;
2692 $function$ LANGUAGE PLPGSQL;
2693
2694
2695 SELECT evergreen.upgrade_deps_block_check('0779', :eg_version);
2696
2697 CREATE TABLE vandelay.import_bib_trash_group(
2698     id SERIAL PRIMARY KEY,
2699     owner INT NOT NULL REFERENCES actor.org_unit(id),
2700     label TEXT NOT NULL, --i18n
2701     always_apply BOOLEAN NOT NULL DEFAULT FALSE,
2702         CONSTRAINT vand_import_bib_trash_grp_owner_label UNIQUE (owner, label)
2703 );
2704
2705 -- otherwise, the ALTER TABLE statement below
2706 -- will fail with pending trigger events.
2707 SET CONSTRAINTS ALL IMMEDIATE;
2708
2709 ALTER TABLE vandelay.import_bib_trash_fields
2710     -- allow null-able for now..
2711     ADD COLUMN grp INTEGER REFERENCES vandelay.import_bib_trash_group;
2712
2713 -- add any existing trash_fields to "Legacy" groups (one per unique field
2714 -- owner) as part of the upgrade, since grp is now required.
2715 -- note that vandelay.import_bib_trash_fields was never used before,
2716 -- so in most cases this should be a no-op.
2717
2718 INSERT INTO vandelay.import_bib_trash_group (owner, label)
2719     SELECT DISTINCT(owner), 'Legacy' FROM vandelay.import_bib_trash_fields;
2720
2721 UPDATE vandelay.import_bib_trash_fields field SET grp = tgroup.id
2722     FROM vandelay.import_bib_trash_group tgroup
2723     WHERE tgroup.owner = field.owner;
2724     
2725 ALTER TABLE vandelay.import_bib_trash_fields
2726     -- now that have values, we can make this non-null
2727     ALTER COLUMN grp SET NOT NULL,
2728     -- drop outdated constraint
2729     DROP CONSTRAINT vand_import_bib_trash_fields_idx,
2730     -- owner is implied by the grp
2731     DROP COLUMN owner, 
2732     -- make grp+field unique
2733     ADD CONSTRAINT vand_import_bib_trash_fields_once_per UNIQUE (grp, field);
2734
2735
2736 SELECT evergreen.upgrade_deps_block_check('0780', :eg_version);
2737
2738 ALTER TABLE acq.distribution_formula_entry
2739     ADD COLUMN fund INT REFERENCES acq.fund (id),
2740     ADD COLUMN circ_modifier TEXT REFERENCES config.circ_modifier (code),
2741     ADD COLUMN collection_code TEXT ;
2742
2743
2744 -- support option to roll distribution formula funds
2745 CREATE OR REPLACE FUNCTION acq.rollover_funds_by_org_tree(
2746         old_year INTEGER,
2747         user_id INTEGER,
2748         org_unit_id INTEGER,
2749     encumb_only BOOL DEFAULT FALSE,
2750     include_desc BOOL DEFAULT TRUE
2751 ) RETURNS VOID AS $$
2752 DECLARE
2753 --
2754 new_fund    INT;
2755 new_year    INT := old_year + 1;
2756 org_found   BOOL;
2757 perm_ous    BOOL;
2758 xfer_amount NUMERIC := 0;
2759 roll_fund   RECORD;
2760 deb         RECORD;
2761 detail      RECORD;
2762 roll_distrib_forms BOOL;
2763 --
2764 BEGIN
2765         --
2766         -- Sanity checks
2767         --
2768         IF old_year IS NULL THEN
2769                 RAISE EXCEPTION 'Input year argument is NULL';
2770     ELSIF old_year NOT BETWEEN 2008 and 2200 THEN
2771         RAISE EXCEPTION 'Input year is out of range';
2772         END IF;
2773         --
2774         IF user_id IS NULL THEN
2775                 RAISE EXCEPTION 'Input user id argument is NULL';
2776         END IF;
2777         --
2778         IF org_unit_id IS NULL THEN
2779                 RAISE EXCEPTION 'Org unit id argument is NULL';
2780         ELSE
2781                 --
2782                 -- Validate the org unit
2783                 --
2784                 SELECT TRUE
2785                 INTO org_found
2786                 FROM actor.org_unit
2787                 WHERE id = org_unit_id;
2788                 --
2789                 IF org_found IS NULL THEN
2790                         RAISE EXCEPTION 'Org unit id % is invalid', org_unit_id;
2791                 ELSIF encumb_only THEN
2792                         SELECT INTO perm_ous value::BOOL FROM
2793                         actor.org_unit_ancestor_setting(
2794                                 'acq.fund.allow_rollover_without_money', org_unit_id
2795                         );
2796                         IF NOT FOUND OR NOT perm_ous THEN
2797                                 RAISE EXCEPTION 'Encumbrance-only rollover not permitted at org %', org_unit_id;
2798                         END IF;
2799                 END IF;
2800         END IF;
2801         --
2802         -- Loop over the propagable funds to identify the details
2803         -- from the old fund plus the id of the new one, if it exists.
2804         --
2805         FOR roll_fund in
2806         SELECT
2807             oldf.id AS old_fund,
2808             oldf.org,
2809             oldf.name,
2810             oldf.currency_type,
2811             oldf.code,
2812                 oldf.rollover,
2813             newf.id AS new_fund_id
2814         FROM
2815         acq.fund AS oldf
2816         LEFT JOIN acq.fund AS newf
2817                 ON ( oldf.code = newf.code )
2818         WHERE
2819                     oldf.year = old_year
2820                 AND oldf.propagate
2821         AND newf.year = new_year
2822                 AND ( ( include_desc AND oldf.org IN ( SELECT id FROM actor.org_unit_descendants( org_unit_id ) ) )
2823                 OR (NOT include_desc AND oldf.org = org_unit_id ) )
2824         LOOP
2825                 --RAISE NOTICE 'Processing fund %', roll_fund.old_fund;
2826                 --
2827                 IF roll_fund.new_fund_id IS NULL THEN
2828                         --
2829                         -- The old fund hasn't been propagated yet.  Propagate it now.
2830                         --
2831                         INSERT INTO acq.fund (
2832                                 org,
2833                                 name,
2834                                 year,
2835                                 currency_type,
2836                                 code,
2837                                 rollover,
2838                                 propagate,
2839                                 balance_warning_percent,
2840                                 balance_stop_percent
2841                         ) VALUES (
2842                                 roll_fund.org,
2843                                 roll_fund.name,
2844                                 new_year,
2845                                 roll_fund.currency_type,
2846                                 roll_fund.code,
2847                                 true,
2848                                 true,
2849                                 roll_fund.balance_warning_percent,
2850                                 roll_fund.balance_stop_percent
2851                         )
2852                         RETURNING id INTO new_fund;
2853                 ELSE
2854                         new_fund = roll_fund.new_fund_id;
2855                 END IF;
2856                 --
2857                 -- Determine the amount to transfer
2858                 --
2859                 SELECT amount
2860                 INTO xfer_amount
2861                 FROM acq.fund_spent_balance
2862                 WHERE fund = roll_fund.old_fund;
2863                 --
2864                 IF xfer_amount <> 0 THEN
2865                         IF NOT encumb_only AND roll_fund.rollover THEN
2866                                 --
2867                                 -- Transfer balance from old fund to new
2868                                 --
2869                                 --RAISE NOTICE 'Transferring % from fund % to %', xfer_amount, roll_fund.old_fund, new_fund;
2870                                 --
2871                                 PERFORM acq.transfer_fund(
2872                                         roll_fund.old_fund,
2873                                         xfer_amount,
2874                                         new_fund,
2875                                         xfer_amount,
2876                                         user_id,
2877                                         'Rollover'
2878                                 );
2879                         ELSE
2880                                 --
2881                                 -- Transfer balance from old fund to the void
2882                                 --
2883                                 -- RAISE NOTICE 'Transferring % from fund % to the void', xfer_amount, roll_fund.old_fund;
2884                                 --
2885                                 PERFORM acq.transfer_fund(
2886                                         roll_fund.old_fund,
2887                                         xfer_amount,
2888                                         NULL,
2889                                         NULL,
2890                                         user_id,
2891                                         'Rollover into the void'
2892                                 );
2893                         END IF;
2894                 END IF;
2895                 --
2896                 IF roll_fund.rollover THEN
2897                         --
2898                         -- Move any lineitems from the old fund to the new one
2899                         -- where the associated debit is an encumbrance.
2900                         --
2901                         -- Any other tables tying expenditure details to funds should
2902                         -- receive similar treatment.  At this writing there are none.
2903                         --
2904                         UPDATE acq.lineitem_detail
2905                         SET fund = new_fund
2906                         WHERE
2907                         fund = roll_fund.old_fund -- this condition may be redundant
2908                         AND fund_debit in
2909                         (
2910                                 SELECT id
2911                                 FROM acq.fund_debit
2912                                 WHERE
2913                                 fund = roll_fund.old_fund
2914                                 AND encumbrance
2915                         );
2916                         --
2917                         -- Move encumbrance debits from the old fund to the new fund
2918                         --
2919                         UPDATE acq.fund_debit
2920                         SET fund = new_fund
2921                         wHERE
2922                                 fund = roll_fund.old_fund
2923                                 AND encumbrance;
2924                 END IF;
2925
2926                 -- Rollover distribution formulae funds
2927                 SELECT INTO roll_distrib_forms value::BOOL FROM
2928                         actor.org_unit_ancestor_setting(
2929                                 'acq.fund.rollover_distrib_forms', org_unit_id
2930                         );
2931
2932                 IF roll_distrib_forms THEN
2933                         UPDATE acq.distribution_formula_entry 
2934                                 SET fund = roll_fund.new_fund_id
2935                                 WHERE fund = roll_fund.old_fund;
2936                 END IF;
2937
2938