Constrain serial.issuance.holding_code to be valid JSON or NULL
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.1-2.2-upgrade-db.sql
1 --Upgrade Script for 2.1 to 2.2-alpha2
2
3 -- Don't require use of -vegversion=something
4 \set eg_version '''2.2'''
5
6 -- DROP objects that might have existed from a prior run of 0526
7 -- Yes this is ironic.
8 DROP TABLE IF EXISTS config.db_patch_dependencies;
9 ALTER TABLE config.upgrade_log DROP COLUMN applied_to;
10 DROP FUNCTION evergreen.upgrade_list_applied_deprecates(TEXT);
11 DROP FUNCTION evergreen.upgrade_list_applied_supersedes(TEXT);
12
13 BEGIN;
14 INSERT INTO config.upgrade_log (version) VALUES ('2.2-beta2');
15
16 INSERT INTO config.upgrade_log (version) VALUES ('0526'); --miker
17
18 CREATE TABLE config.db_patch_dependencies (
19   db_patch      TEXT PRIMARY KEY,
20   supersedes    TEXT[],
21   deprecates    TEXT[]
22 );
23
24 CREATE OR REPLACE FUNCTION evergreen.array_overlap_check (/* field */) RETURNS TRIGGER AS $$
25 DECLARE
26     fld     TEXT;
27     cnt     INT;
28 BEGIN
29     fld := TG_ARGV[1];
30     EXECUTE 'SELECT COUNT(*) FROM '|| TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME ||' WHERE '|| fld ||' && ($1).'|| fld INTO cnt USING NEW;
31     IF cnt > 0 THEN
32         RAISE EXCEPTION 'Cannot insert duplicate array into field % of table %', fld, TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME;
33     END IF;
34     RETURN NEW;
35 END;
36 $$ LANGUAGE PLPGSQL;
37
38 CREATE TRIGGER no_overlapping_sups
39     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
40     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('supersedes');
41
42 CREATE TRIGGER no_overlapping_deps
43     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
44     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
45
46 ALTER TABLE config.upgrade_log
47     ADD COLUMN applied_to TEXT;
48
49 -- Provide a named type for patching functions
50 CREATE TYPE evergreen.patch AS (patch TEXT);
51
52 -- List applied db patches that are deprecated by (and block the application of) my_db_patch
53 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecates ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
54     SELECT  DISTINCT l.version
55       FROM  config.upgrade_log l
56             JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.deprecates)
57       WHERE d.db_patch = $1
58 $$ LANGUAGE SQL;
59
60 -- List applied db patches that are superseded by (and block the application of) my_db_patch
61 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_supersedes ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
62     SELECT  DISTINCT l.version
63       FROM  config.upgrade_log l
64             JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.supersedes)
65       WHERE d.db_patch = $1
66 $$ LANGUAGE SQL;
67
68 -- List applied db patches that deprecates (and block the application of) my_db_patch
69 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecated ( my_db_patch TEXT ) RETURNS TEXT AS $$
70     SELECT  db_patch
71       FROM  config.db_patch_dependencies
72       WHERE ARRAY[$1]::TEXT[] && deprecates
73 $$ LANGUAGE SQL;
74
75 -- List applied db patches that supersedes (and block the application of) my_db_patch
76 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_superseded ( my_db_patch TEXT ) RETURNS TEXT AS $$
77     SELECT  db_patch
78       FROM  config.db_patch_dependencies
79       WHERE ARRAY[$1]::TEXT[] && supersedes
80 $$ LANGUAGE SQL;
81
82 -- Make sure that no deprecated or superseded db patches are currently applied
83 CREATE OR REPLACE FUNCTION evergreen.upgrade_verify_no_dep_conflicts ( my_db_patch TEXT ) RETURNS BOOL AS $$
84     SELECT  COUNT(*) = 0
85       FROM  (SELECT * FROM evergreen.upgrade_list_applied_deprecates( $1 )
86                 UNION
87              SELECT * FROM evergreen.upgrade_list_applied_supersedes( $1 )
88                 UNION
89              SELECT * FROM evergreen.upgrade_list_applied_deprecated( $1 )
90                 UNION
91              SELECT * FROM evergreen.upgrade_list_applied_superseded( $1 ))x
92 $$ LANGUAGE SQL;
93
94 -- Raise an exception if there are, in fact, dep/sup confilct
95 CREATE OR REPLACE FUNCTION evergreen.upgrade_deps_block_check ( my_db_patch TEXT, my_applied_to TEXT ) RETURNS BOOL AS $$
96 DECLARE 
97     deprecates TEXT;
98     supersedes TEXT;
99 BEGIN
100     IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
101         SELECT  STRING_AGG(patch, ', ') INTO deprecates FROM evergreen.upgrade_list_applied_deprecates(my_db_patch);
102         SELECT  STRING_AGG(patch, ', ') INTO supersedes FROM evergreen.upgrade_list_applied_supersedes(my_db_patch);
103         RAISE EXCEPTION '
104 Upgrade script % can not be applied:
105   applied deprecated scripts %
106   applied superseded scripts %
107   deprecated by %
108   superseded by %',
109             my_db_patch,
110             ARRAY_AGG(evergreen.upgrade_list_applied_deprecates(my_db_patch)),
111             ARRAY_AGG(evergreen.upgrade_list_applied_supersedes(my_db_patch)),
112             evergreen.upgrade_list_applied_deprecated(my_db_patch),
113             evergreen.upgrade_list_applied_superseded(my_db_patch);
114     END IF;
115
116     INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
117     RETURN TRUE;
118 END;
119 $$ LANGUAGE PLPGSQL;
120
121 -- Evergreen DB patch 0536.schema.lazy_circ-barcode_lookup.sql
122 --
123 -- FIXME: insert description of change, if needed
124 --
125
126 -- check whether patch can be applied
127 INSERT INTO config.upgrade_log (version) VALUES ('0536');
128
129 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype) VALUES ( 'circ.staff_client.actor_on_checkout', 'Load patron from Checkout', 'When scanning barcodes into Checkout auto-detect if a new patron barcode is scanned and auto-load the new patron.', 'bool');
130
131 CREATE TABLE config.barcode_completion (
132     id          SERIAL  PRIMARY KEY,
133     active      BOOL    NOT NULL DEFAULT true,
134     org_unit    INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
135     prefix      TEXT,
136     suffix      TEXT,
137     length      INT     NOT NULL DEFAULT 0,
138     padding     TEXT,
139     padding_end BOOL    NOT NULL DEFAULT false,
140     asset       BOOL    NOT NULL DEFAULT true,
141     actor       BOOL    NOT NULL DEFAULT true
142 );
143
144 CREATE TYPE evergreen.barcode_set AS (type TEXT, id BIGINT, barcode TEXT);
145
146 CREATE OR REPLACE FUNCTION evergreen.get_barcodes(select_ou INT, type TEXT, in_barcode TEXT) RETURNS SETOF evergreen.barcode_set AS $$
147 DECLARE
148     cur_barcode TEXT;
149     barcode_len INT;
150     completion_len  INT;
151     asset_barcodes  TEXT[];
152     actor_barcodes  TEXT[];
153     do_asset    BOOL = false;
154     do_serial   BOOL = false;
155     do_booking  BOOL = false;
156     do_actor    BOOL = false;
157     completion_set  config.barcode_completion%ROWTYPE;
158 BEGIN
159
160     IF position('asset' in type) > 0 THEN
161         do_asset = true;
162     END IF;
163     IF position('serial' in type) > 0 THEN
164         do_serial = true;
165     END IF;
166     IF position('booking' in type) > 0 THEN
167         do_booking = true;
168     END IF;
169     IF do_asset OR do_serial OR do_booking THEN
170         asset_barcodes = asset_barcodes || in_barcode;
171     END IF;
172     IF position('actor' in type) > 0 THEN
173         do_actor = true;
174         actor_barcodes = actor_barcodes || in_barcode;
175     END IF;
176
177     barcode_len := length(in_barcode);
178
179     FOR completion_set IN
180       SELECT * FROM config.barcode_completion
181         WHERE active
182         AND org_unit IN (SELECT aou.id FROM actor.org_unit_ancestors(select_ou) aou)
183         LOOP
184         IF completion_set.prefix IS NULL THEN
185             completion_set.prefix := '';
186         END IF;
187         IF completion_set.suffix IS NULL THEN
188             completion_set.suffix := '';
189         END IF;
190         IF completion_set.length = 0 OR completion_set.padding IS NULL OR length(completion_set.padding) = 0 THEN
191             cur_barcode = completion_set.prefix || in_barcode || completion_set.suffix;
192         ELSE
193             completion_len = completion_set.length - length(completion_set.prefix) - length(completion_set.suffix);
194             IF completion_len >= barcode_len THEN
195                 IF completion_set.padding_end THEN
196                     cur_barcode = rpad(in_barcode, completion_len, completion_set.padding);
197                 ELSE
198                     cur_barcode = lpad(in_barcode, completion_len, completion_set.padding);
199                 END IF;
200                 cur_barcode = completion_set.prefix || cur_barcode || completion_set.suffix;
201             END IF;
202         END IF;
203         IF completion_set.actor THEN
204             actor_barcodes = actor_barcodes || cur_barcode;
205         END IF;
206         IF completion_set.asset THEN
207             asset_barcodes = asset_barcodes || cur_barcode;
208         END IF;
209     END LOOP;
210
211     IF do_asset AND do_serial THEN
212         RETURN QUERY SELECT 'asset'::TEXT, id, barcode FROM ONLY asset.copy WHERE barcode = ANY(asset_barcodes) AND deleted = false;
213         RETURN QUERY SELECT 'serial'::TEXT, id, barcode FROM serial.unit WHERE barcode = ANY(asset_barcodes) AND deleted = false;
214     ELSIF do_asset THEN
215         RETURN QUERY SELECT 'asset'::TEXT, id, barcode FROM asset.copy WHERE barcode = ANY(asset_barcodes) AND deleted = false;
216     ELSIF do_serial THEN
217         RETURN QUERY SELECT 'serial'::TEXT, id, barcode FROM serial.unit WHERE barcode = ANY(asset_barcodes) AND deleted = false;
218     END IF;
219     IF do_booking THEN
220         RETURN QUERY SELECT 'booking'::TEXT, id::BIGINT, barcode FROM booking.resource WHERE barcode = ANY(asset_barcodes);
221     END IF;
222     IF do_actor THEN
223         RETURN QUERY SELECT 'actor'::TEXT, c.usr::BIGINT, c.barcode FROM actor.card c JOIN actor.usr u ON c.usr = u.id WHERE c.barcode = ANY(actor_barcodes) AND c.active AND NOT u.deleted ORDER BY usr;
224     END IF;
225     RETURN;
226 END;
227 $$ LANGUAGE plpgsql;
228
229 COMMENT ON FUNCTION evergreen.get_barcodes(INT, TEXT, TEXT) IS $$
230 Given user input, find an appropriate barcode in the proper class.
231
232 Will add prefix/suffix information to do so, and return all results.
233 $$;
234
235
236
237 INSERT INTO config.upgrade_log (version) VALUES ('0537'); --miker
238
239 DROP FUNCTION evergreen.upgrade_deps_block_check(text,text);
240 DROP FUNCTION evergreen.upgrade_verify_no_dep_conflicts(text);
241 DROP FUNCTION evergreen.upgrade_list_applied_deprecated(text);
242 DROP FUNCTION evergreen.upgrade_list_applied_superseded(text);
243
244 -- List applied db patches that deprecates (and block the application of) my_db_patch
245 CREATE FUNCTION evergreen.upgrade_list_applied_deprecated ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
246     SELECT  db_patch
247       FROM  config.db_patch_dependencies
248       WHERE ARRAY[$1]::TEXT[] && deprecates
249 $$ LANGUAGE SQL;
250
251 -- List applied db patches that supersedes (and block the application of) my_db_patch
252 CREATE FUNCTION evergreen.upgrade_list_applied_superseded ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
253     SELECT  db_patch
254       FROM  config.db_patch_dependencies
255       WHERE ARRAY[$1]::TEXT[] && supersedes
256 $$ LANGUAGE SQL;
257
258 -- Make sure that no deprecated or superseded db patches are currently applied
259 CREATE FUNCTION evergreen.upgrade_verify_no_dep_conflicts ( my_db_patch TEXT ) RETURNS BOOL AS $$
260     SELECT  COUNT(*) = 0
261       FROM  (SELECT * FROM evergreen.upgrade_list_applied_deprecates( $1 )
262                 UNION
263              SELECT * FROM evergreen.upgrade_list_applied_supersedes( $1 )
264                 UNION
265              SELECT * FROM evergreen.upgrade_list_applied_deprecated( $1 )
266                 UNION
267              SELECT * FROM evergreen.upgrade_list_applied_superseded( $1 ))x
268 $$ LANGUAGE SQL;
269
270 -- Raise an exception if there are, in fact, dep/sup confilct
271 CREATE FUNCTION evergreen.upgrade_deps_block_check ( my_db_patch TEXT, my_applied_to TEXT ) RETURNS BOOL AS $$
272 BEGIN
273     IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
274         RAISE EXCEPTION '
275 Upgrade script % can not be applied:
276   applied deprecated scripts %
277   applied superseded scripts %
278   deprecated by %
279   superseded by %',
280             my_db_patch,
281             ARRAY_ACCUM(evergreen.upgrade_list_applied_deprecates(my_db_patch)),
282             ARRAY_ACCUM(evergreen.upgrade_list_applied_supersedes(my_db_patch)),
283             evergreen.upgrade_list_applied_deprecated(my_db_patch),
284             evergreen.upgrade_list_applied_superseded(my_db_patch);
285     END IF;
286
287     INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
288     RETURN TRUE;
289 END;
290 $$ LANGUAGE PLPGSQL;
291
292
293 INSERT INTO config.upgrade_log (version) VALUES ('0544');
294
295 INSERT INTO config.usr_setting_type 
296 ( name, opac_visible, label, description, datatype) VALUES 
297 ( 'circ.collections.exempt',
298   FALSE, 
299   oils_i18n_gettext('circ.collections.exempt', 'Collections: Exempt', 'cust', 'description'),
300   oils_i18n_gettext('circ.collections.exempt', 'User is exempt from collections tracking/processing', 'cust', 'description'),
301   'bool'
302 );
303
304
305
306 SELECT evergreen.upgrade_deps_block_check('0545', :eg_version);
307
308 INSERT INTO permission.perm_list VALUES
309  (507, 'ABORT_TRANSIT_ON_LOST', oils_i18n_gettext(507, 'Allows a user to abort a transit on a copy with status of LOST', 'ppl', 'description')),
310  (508, 'ABORT_TRANSIT_ON_MISSING', oils_i18n_gettext(508, 'Allows a user to abort a transit on a copy with status of MISSING', 'ppl', 'description'));
311
312 --- stock Circulation Administrator group
313
314 INSERT INTO permission.grp_perm_map ( grp, perm, depth, grantable )
315     SELECT
316         4,
317         id,
318         0,
319         't'
320     FROM permission.perm_list
321     WHERE code in ('ABORT_TRANSIT_ON_LOST', 'ABORT_TRANSIT_ON_MISSING');
322
323 -- Evergreen DB patch 0546.schema.sip_statcats.sql
324
325
326 -- check whether patch can be applied
327 SELECT evergreen.upgrade_deps_block_check('0546', :eg_version);
328
329 CREATE TABLE actor.stat_cat_sip_fields (
330     field   CHAR(2) PRIMARY KEY,
331     name    TEXT    NOT NULL,
332     one_only  BOOL    NOT NULL DEFAULT FALSE
333 );
334 COMMENT ON TABLE actor.stat_cat_sip_fields IS $$
335 Actor Statistical Category SIP Fields
336
337 Contains the list of valid SIP Field identifiers for
338 Statistical Categories.
339 $$;
340 ALTER TABLE actor.stat_cat
341     ADD COLUMN sip_field   CHAR(2) REFERENCES actor.stat_cat_sip_fields(field) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
342     ADD COLUMN sip_format  TEXT;
343
344 CREATE FUNCTION actor.stat_cat_check() RETURNS trigger AS $func$
345 DECLARE
346     sipfield actor.stat_cat_sip_fields%ROWTYPE;
347     use_count INT;
348 BEGIN
349     IF NEW.sip_field IS NOT NULL THEN
350         SELECT INTO sipfield * FROM actor.stat_cat_sip_fields WHERE field = NEW.sip_field;
351         IF sipfield.one_only THEN
352             SELECT INTO use_count count(id) FROM actor.stat_cat WHERE sip_field = NEW.sip_field AND id != NEW.id;
353             IF use_count > 0 THEN
354                 RAISE EXCEPTION 'Sip field cannot be used twice';
355             END IF;
356         END IF;
357     END IF;
358     RETURN NEW;
359 END;
360 $func$ LANGUAGE PLPGSQL;
361
362 CREATE TRIGGER actor_stat_cat_sip_update_trigger
363     BEFORE INSERT OR UPDATE ON actor.stat_cat FOR EACH ROW
364     EXECUTE PROCEDURE actor.stat_cat_check();
365
366 CREATE TABLE asset.stat_cat_sip_fields (
367     field   CHAR(2) PRIMARY KEY,
368     name    TEXT    NOT NULL,
369     one_only BOOL    NOT NULL DEFAULT FALSE
370 );
371 COMMENT ON TABLE asset.stat_cat_sip_fields IS $$
372 Asset Statistical Category SIP Fields
373
374 Contains the list of valid SIP Field identifiers for
375 Statistical Categories.
376 $$;
377
378 ALTER TABLE asset.stat_cat
379     ADD COLUMN sip_field   CHAR(2) REFERENCES asset.stat_cat_sip_fields(field) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
380     ADD COLUMN sip_format  TEXT;
381
382 CREATE FUNCTION asset.stat_cat_check() RETURNS trigger AS $func$
383 DECLARE
384     sipfield asset.stat_cat_sip_fields%ROWTYPE;
385     use_count INT;
386 BEGIN
387     IF NEW.sip_field IS NOT NULL THEN
388         SELECT INTO sipfield * FROM asset.stat_cat_sip_fields WHERE field = NEW.sip_field;
389         IF sipfield.one_only THEN
390             SELECT INTO use_count count(id) FROM asset.stat_cat WHERE sip_field = NEW.sip_field AND id != NEW.id;
391             IF use_count > 0 THEN
392                 RAISE EXCEPTION 'Sip field cannot be used twice';
393             END IF;
394         END IF;
395     END IF;
396     RETURN NEW;
397 END;
398 $func$ LANGUAGE PLPGSQL;
399
400 CREATE TRIGGER asset_stat_cat_sip_update_trigger
401     BEFORE INSERT OR UPDATE ON asset.stat_cat FOR EACH ROW
402     EXECUTE PROCEDURE asset.stat_cat_check();
403
404
405
406 SELECT evergreen.upgrade_deps_block_check('0548', :eg_version); -- dbwells
407
408 \qecho This redoes the original part 1 of 0547 which did not apply to rel_2_1,
409 \qecho and is being added for the sake of clarity
410
411 -- delete errant inserts from 0545 (group 4 is NOT the circulation admin group)
412 DELETE FROM permission.grp_perm_map WHERE grp = 4 AND perm IN (
413         SELECT id FROM permission.perm_list
414         WHERE code in ('ABORT_TRANSIT_ON_LOST', 'ABORT_TRANSIT_ON_MISSING')
415 );
416
417 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
418         SELECT
419                 pgt.id, perm.id, aout.depth, TRUE
420         FROM
421                 permission.grp_tree pgt,
422                 permission.perm_list perm,
423                 actor.org_unit_type aout
424         WHERE
425                 pgt.name = 'Circulation Administrator' AND
426                 aout.name = 'Consortium' AND
427                 perm.code IN (
428                         'ABORT_TRANSIT_ON_LOST',
429                         'ABORT_TRANSIT_ON_MISSING'
430                 ) AND NOT EXISTS (
431                         SELECT 1
432                         FROM permission.grp_perm_map AS map
433                         WHERE
434                                 map.grp = pgt.id
435                                 AND map.perm = perm.id
436                 );
437
438 -- Evergreen DB patch XXXX.data.transit-checkin-interval.sql
439 --
440 -- New org unit setting "circ.transit.min_checkin_interval"
441 -- New TRANSIT_CHECKIN_INTERVAL_BLOCK.override permission
442 --
443
444
445 -- check whether patch can be applied
446 SELECT evergreen.upgrade_deps_block_check('0549', :eg_version);
447
448 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
449     'circ.transit.min_checkin_interval',
450     oils_i18n_gettext( 
451         'circ.transit.min_checkin_interval', 
452         'Circ:  Minimum Transit Checkin Interval',
453         'coust',
454         'label'
455     ),
456     oils_i18n_gettext( 
457         'circ.transit.min_checkin_interval', 
458         'In-Transit items checked in this close to the transit start time will be prevented from checking in',
459         'coust',
460         'label'
461     ),
462     'interval'
463 );
464
465 INSERT INTO permission.perm_list ( id, code, description ) VALUES (  
466     509, 
467     'TRANSIT_CHECKIN_INTERVAL_BLOCK.override', 
468     oils_i18n_gettext(
469         509,
470         'Allows a user to override the TRANSIT_CHECKIN_INTERVAL_BLOCK event', 
471         'ppl', 
472         'description'
473     )
474 );
475
476 -- add the perm to the default circ admin group
477 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
478         SELECT
479                 pgt.id, perm.id, aout.depth, TRUE
480         FROM
481                 permission.grp_tree pgt,
482                 permission.perm_list perm,
483                 actor.org_unit_type aout
484         WHERE
485                 pgt.name = 'Circulation Administrator' AND
486                 aout.name = 'System' AND
487                 perm.code IN ( 'TRANSIT_CHECKIN_INTERVAL_BLOCK.override' );
488
489
490 -- check whether patch can be applied
491 SELECT evergreen.upgrade_deps_block_check('0550', :eg_version);
492
493 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
494     'org.patron_opt_boundary',
495     oils_i18n_gettext( 
496         'org.patron_opt_boundary',
497         'Circ: Patron Opt-In Boundary',
498         'coust',
499         'label'
500     ),
501     oils_i18n_gettext( 
502         'org.patron_opt_boundary',
503         'This determines at which depth above which patrons must be opted in, and below which patrons will be assumed to be opted in.',
504         'coust',
505         'label'
506     ),
507     'integer'
508 );
509
510 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
511     'org.patron_opt_default',
512     oils_i18n_gettext( 
513         'org.patron_opt_default',
514         'Circ: Patron Opt-In Default',
515         'coust',
516         'label'
517     ),
518     oils_i18n_gettext( 
519         'org.patron_opt_default',
520         'This is the default depth at which a patron is opted in; it is calculated as an org unit relative to the current workstation.',
521         'coust',
522         'label'
523     ),
524     'integer'
525 );
526
527 -- Evergreen DB patch 0562.schema.copy_active_date.sql
528 --
529 -- Active Date
530
531
532 -- check whether patch can be applied
533 SELECT evergreen.upgrade_deps_block_check('0562', :eg_version);
534
535 ALTER TABLE asset.copy
536     ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
537
538 ALTER TABLE auditor.asset_copy_history
539     ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
540
541 ALTER TABLE auditor.serial_unit_history
542     ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
543
544 ALTER TABLE config.copy_status
545     ADD COLUMN copy_active BOOL NOT NULL DEFAULT FALSE;
546
547 ALTER TABLE config.circ_matrix_weights
548     ADD COLUMN item_age NUMERIC(6,2) NOT NULL DEFAULT 0.0;
549
550 ALTER TABLE config.hold_matrix_weights
551     ADD COLUMN item_age NUMERIC(6,2) NOT NULL DEFAULT 0.0;
552
553 -- The two defaults above were to stop erroring on NOT NULL
554 -- Remove them here
555 ALTER TABLE config.circ_matrix_weights
556     ALTER COLUMN item_age DROP DEFAULT;
557
558 ALTER TABLE config.hold_matrix_weights
559     ALTER COLUMN item_age DROP DEFAULT;
560
561 ALTER TABLE config.circ_matrix_matchpoint
562     ADD COLUMN item_age INTERVAL;
563
564 ALTER TABLE config.hold_matrix_matchpoint
565     ADD COLUMN item_age INTERVAL;
566
567 --Removed dupe asset.acp_status_changed
568
569 CREATE OR REPLACE FUNCTION asset.acp_created()
570 RETURNS TRIGGER AS $$
571 BEGIN
572     IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
573         NEW.active_date := now();
574     END IF;
575     IF NEW.status_changed_time IS NULL THEN
576         NEW.status_changed_time := now();
577     END IF;
578     RETURN NEW;
579 END;
580 $$ LANGUAGE plpgsql;
581
582 CREATE TRIGGER acp_created_trig
583     BEFORE INSERT ON asset.copy
584     FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
585
586 CREATE TRIGGER sunit_created_trig
587     BEFORE INSERT ON serial.unit
588     FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
589
590 --Removed dupe action.hold_request_permit_test
591
592 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$
593 DECLARE
594     cn_object       asset.call_number%ROWTYPE;
595     rec_descriptor  metabib.rec_descriptor%ROWTYPE;
596     cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
597     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
598     weights         config.circ_matrix_weights%ROWTYPE;
599     user_age        INTERVAL;
600     my_item_age     INTERVAL;
601     denominator     NUMERIC(6,2);
602     row_list        INT[];
603     result          action.found_circ_matrix_matchpoint;
604 BEGIN
605     -- Assume failure
606     result.success = false;
607
608     -- Fetch useful data
609     SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
610     SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
611
612     -- Pre-generate this so we only calc it once
613     IF user_object.dob IS NOT NULL THEN
614         SELECT INTO user_age age(user_object.dob);
615     END IF;
616
617     -- Ditto
618     SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
619
620     -- Grab the closest set circ weight setting.
621     SELECT INTO weights cw.*
622       FROM config.weight_assoc wa
623            JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
624            JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
625       WHERE active
626       ORDER BY d.distance
627       LIMIT 1;
628
629     -- No weights? Bad admin! Defaults to handle that anyway.
630     IF weights.id IS NULL THEN
631         weights.grp                 := 11.0;
632         weights.org_unit            := 10.0;
633         weights.circ_modifier       := 5.0;
634         weights.marc_type           := 4.0;
635         weights.marc_form           := 3.0;
636         weights.marc_bib_level      := 2.0;
637         weights.marc_vr_format      := 2.0;
638         weights.copy_circ_lib       := 8.0;
639         weights.copy_owning_lib     := 8.0;
640         weights.user_home_ou        := 8.0;
641         weights.ref_flag            := 1.0;
642         weights.juvenile_flag       := 6.0;
643         weights.is_renewal          := 7.0;
644         weights.usr_age_lower_bound := 0.0;
645         weights.usr_age_upper_bound := 0.0;
646         weights.item_age            := 0.0;
647     END IF;
648
649     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
650     -- If you break your org tree with funky parenting this may be wrong
651     -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
652     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
653     WITH all_distance(distance) AS (
654             SELECT depth AS distance FROM actor.org_unit_type
655         UNION
656             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
657         )
658     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
659
660     -- Loop over all the potential matchpoints
661     FOR cur_matchpoint IN
662         SELECT m.*
663           FROM  config.circ_matrix_matchpoint m
664                 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
665                 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
666                 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
667                 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
668                 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
669           WHERE m.active
670                 -- Permission Groups
671              -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
672                 -- Org Units
673              -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
674                 AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
675                 AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
676                 AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
677                 -- Circ Type
678                 AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
679                 -- Static User Checks
680                 AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
681                 AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
682                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
683                 -- Static Item Checks
684                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
685                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
686                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
687                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
688                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
689                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
690                 AND (m.item_age                 IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
691           ORDER BY
692                 -- Permission Groups
693                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
694                 -- Org Units
695                 CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
696                 CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
697                 CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
698                 CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
699                 -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
700                 CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
701                 -- Static User Checks
702                 CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
703                 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
704                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
705                 -- Static Item Checks
706                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
707                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
708                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
709                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
710                 CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
711                 -- Item age has a slight adjustment to weight based on value.
712                 -- This should ensure that a shorter age limit comes first when all else is equal.
713                 -- NOTE: This assumes that intervals will normally be in days.
714                 CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 1 + 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
715                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
716                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
717                 m.id LOOP
718
719         -- Record the full matching row list
720         row_list := row_list || cur_matchpoint.id;
721
722         -- No matchpoint yet?
723         IF matchpoint.id IS NULL THEN
724             -- Take the entire matchpoint as a starting point
725             matchpoint := cur_matchpoint;
726             CONTINUE; -- No need to look at this row any more.
727         END IF;
728
729         -- Incomplete matchpoint?
730         IF matchpoint.circulate IS NULL THEN
731             matchpoint.circulate := cur_matchpoint.circulate;
732         END IF;
733         IF matchpoint.duration_rule IS NULL THEN
734             matchpoint.duration_rule := cur_matchpoint.duration_rule;
735         END IF;
736         IF matchpoint.recurring_fine_rule IS NULL THEN
737             matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
738         END IF;
739         IF matchpoint.max_fine_rule IS NULL THEN
740             matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
741         END IF;
742         IF matchpoint.hard_due_date IS NULL THEN
743             matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
744         END IF;
745         IF matchpoint.total_copy_hold_ratio IS NULL THEN
746             matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
747         END IF;
748         IF matchpoint.available_copy_hold_ratio IS NULL THEN
749             matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
750         END IF;
751         IF matchpoint.renewals IS NULL THEN
752             matchpoint.renewals := cur_matchpoint.renewals;
753         END IF;
754         IF matchpoint.grace_period IS NULL THEN
755             matchpoint.grace_period := cur_matchpoint.grace_period;
756         END IF;
757     END LOOP;
758
759     -- Check required fields
760     IF matchpoint.circulate             IS NOT NULL AND
761        matchpoint.duration_rule         IS NOT NULL AND
762        matchpoint.recurring_fine_rule   IS NOT NULL AND
763        matchpoint.max_fine_rule         IS NOT NULL THEN
764         -- All there? We have a completed match.
765         result.success := true;
766     END IF;
767
768     -- Include the assembled matchpoint, even if it isn't complete
769     result.matchpoint := matchpoint;
770
771     -- Include (for debugging) the full list of matching rows
772     result.buildrows := row_list;
773
774     -- Hand the result back to caller
775     RETURN result;
776 END;
777 $func$ LANGUAGE plpgsql;
778
779 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
780   RETURNS integer AS
781 $func$
782 DECLARE
783     requestor_object    actor.usr%ROWTYPE;
784     user_object         actor.usr%ROWTYPE;
785     item_object         asset.copy%ROWTYPE;
786     item_cn_object      asset.call_number%ROWTYPE;
787     my_item_age         INTERVAL;
788     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
789     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
790     weights             config.hold_matrix_weights%ROWTYPE;
791     denominator         NUMERIC(6,2);
792 BEGIN
793     SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
794     SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
795     SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
796     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
797     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
798
799     SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
800
801     -- The item's owner should probably be the one determining if the item is holdable
802     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
803     -- This flag will allow for setting it to the owning library (where the call number "lives")
804     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
805
806     -- Grab the closest set circ weight setting.
807     IF NOT FOUND THEN
808         -- Default to circ library
809         SELECT INTO weights hw.*
810           FROM config.weight_assoc wa
811                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
812                JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
813           WHERE active
814           ORDER BY d.distance
815           LIMIT 1;
816     ELSE
817         -- Flag is set, use owning library
818         SELECT INTO weights hw.*
819           FROM config.weight_assoc wa
820                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
821                JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) d ON (wa.org_unit = d.id)
822           WHERE active
823           ORDER BY d.distance
824           LIMIT 1;
825     END IF;
826
827     -- No weights? Bad admin! Defaults to handle that anyway.
828     IF weights.id IS NULL THEN
829         weights.user_home_ou    := 5.0;
830         weights.request_ou      := 5.0;
831         weights.pickup_ou       := 5.0;
832         weights.item_owning_ou  := 5.0;
833         weights.item_circ_ou    := 5.0;
834         weights.usr_grp         := 7.0;
835         weights.requestor_grp   := 8.0;
836         weights.circ_modifier   := 4.0;
837         weights.marc_type       := 3.0;
838         weights.marc_form       := 2.0;
839         weights.marc_bib_level  := 1.0;
840         weights.marc_vr_format  := 1.0;
841         weights.juvenile_flag   := 4.0;
842         weights.ref_flag        := 0.0;
843         weights.item_age        := 0.0;
844     END IF;
845
846     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
847     -- If you break your org tree with funky parenting this may be wrong
848     -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
849     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
850     WITH all_distance(distance) AS (
851             SELECT depth AS distance FROM actor.org_unit_type
852         UNION
853             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
854         )
855     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
856
857     -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
858     -- This may be better implemented as part of the upgrade script?
859     -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
860     -- Then remove this flag, of course.
861     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
862
863     IF FOUND THEN
864         -- Note: This, to me, is REALLY hacky. I put it in anyway.
865         -- If you can't tell, this is a single call swap on two variables.
866         SELECT INTO user_object.profile, requestor_object.profile
867                     requestor_object.profile, user_object.profile;
868     END IF;
869
870     -- Select the winning matchpoint into the matchpoint variable for returning
871     SELECT INTO matchpoint m.*
872       FROM  config.hold_matrix_matchpoint m
873             /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
874             LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
875             LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
876             LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
877             LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
878             LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
879             LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
880       WHERE m.active
881             -- Permission Groups
882          -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
883             AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
884             -- Org Units
885             AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
886             AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
887             AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
888             AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
889             AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
890             -- Static User Checks
891             AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
892             -- Static Item Checks
893             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
894             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
895             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
896             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
897             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
898             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
899             AND (m.item_age             IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
900       ORDER BY
901             -- Permission Groups
902             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
903             CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
904             -- Org Units
905             CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
906             CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
907             CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
908             CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
909             CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
910             -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
911             CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
912             -- Static Item Checks
913             CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
914             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
915             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
916             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
917             CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
918             -- Item age has a slight adjustment to weight based on value.
919             -- This should ensure that a shorter age limit comes first when all else is equal.
920             -- NOTE: This assumes that intervals will normally be in days.
921             CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
922             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
923             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
924             m.id;
925
926     -- Return just the ID for now
927     RETURN matchpoint.id;
928 END;
929 $func$ LANGUAGE 'plpgsql';
930
931 DROP INDEX IF EXISTS config.ccmm_once_per_paramset;
932
933 DROP INDEX IF EXISTS config.chmm_once_per_paramset;
934
935 CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
936
937 CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
938
939 UPDATE config.copy_status SET copy_active = true WHERE id IN (0, 1, 7, 8, 10, 12, 15);
940
941 INSERT into config.org_unit_setting_type
942 ( name, label, description, datatype ) VALUES
943 ( 'circ.holds.age_protect.active_date', 'Holds: Use Active Date for Age Protection', 'When calculating age protection rules use the active date instead of the creation date.', 'bool');
944
945 -- Assume create date when item is in status we would update active date for anyway
946 UPDATE asset.copy SET active_date = create_date WHERE status IN (SELECT id FROM config.copy_status WHERE copy_active = true);
947
948 -- Assume create date for any item with circs
949 UPDATE asset.copy SET active_date = create_date WHERE id IN (SELECT id FROM extend_reporter.full_circ_count WHERE circ_count > 0);
950
951 -- Assume create date for status change time while we are at it. Because being created WAS a change in status.
952 UPDATE asset.copy SET status_changed_time = create_date WHERE status_changed_time IS NULL;
953
954 -- Evergreen DB patch 0564.data.delete_empty_volume.sql
955 --
956 -- New org setting cat.volume.delete_on_empty
957 --
958
959 -- check whether patch can be applied
960 SELECT evergreen.upgrade_deps_block_check('0564', :eg_version);
961
962 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) 
963     VALUES ( 
964         'cat.volume.delete_on_empty',
965         oils_i18n_gettext('cat.volume.delete_on_empty', 'Cat: Delete volume with last copy', 'coust', 'label'),
966         oils_i18n_gettext('cat.volume.delete_on_empty', 'Automatically delete a volume when the last linked copy is deleted', 'coust', 'description'),
967         'bool'
968     );
969
970
971 -- Evergreen DB patch 0565.schema.action-trigger.event_definition.hold-cancel-no-target-notification.sql
972 --
973 -- New action trigger event definition: Hold Cancelled (No Target) Email Notification
974 --
975
976 -- check whether patch can be applied
977 SELECT evergreen.upgrade_deps_block_check('0565', :eg_version);
978
979 INSERT INTO action_trigger.event_definition (id, active, owner, name, hook, validator, reactor, delay, delay_field, group_field, template)
980     VALUES (38, FALSE, 1, 
981         'Hold Cancelled (No Target) Email Notification', 
982         'hold_request.cancel.expire_no_target', 
983         'HoldIsCancelled', 'SendEmail', '30 minutes', 'cancel_time', 'usr',
984 $$
985 [%- USE date -%]
986 [%- user = target.0.usr -%]
987 To: [%- params.recipient_email || user.email %]
988 From: [%- params.sender_email || default_sender %]
989 Subject: Hold Request Cancelled
990
991 Dear [% user.family_name %], [% user.first_given_name %]
992 The following holds were cancelled because no items were found to fullfil the hold.
993
994 [% FOR hold IN target %]
995     Title: [% hold.bib_rec.bib_record.simple_record.title %]
996     Author: [% hold.bib_rec.bib_record.simple_record.author %]
997     Library: [% hold.pickup_lib.name %]
998     Request Date: [% date.format(helpers.format_date(hold.rrequest_time), '%Y-%m-%d') %]
999 [% END %]
1000
1001 $$);
1002
1003 INSERT INTO action_trigger.environment (event_def, path) VALUES
1004     (38, 'usr'),
1005     (38, 'pickup_lib'),
1006     (38, 'bib_rec.bib_record.simple_record');
1007
1008 -- Evergreen DB patch XXXX.data.ou_setting_generate_overdue_on_lost.sql.sql
1009
1010 -- check whether patch can be applied
1011 SELECT evergreen.upgrade_deps_block_check('0567', :eg_version);
1012
1013 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
1014     'circ.lost.generate_overdue_on_checkin',
1015     oils_i18n_gettext( 
1016         'circ.lost.generate_overdue_on_checkin',
1017         'Circ:  Lost Checkin Generates New Overdues',
1018         'coust',
1019         'label'
1020     ),
1021     oils_i18n_gettext( 
1022         'circ.lost.generate_overdue_on_checkin',
1023         'Enabling this setting causes retroactive creation of not-yet-existing overdue fines on lost item checkin, up to the point of checkin time (or max fines is reached).  This is different than "restore overdue on lost", because it only creates new overdue fines.  Use both settings together to get the full complement of overdue fines for a lost item',
1024         'coust',
1025         'label'
1026     ),
1027     'bool'
1028 );
1029
1030 -- Evergreen DB patch 0572.vandelay-record-matching-and-quality.sql
1031 --
1032
1033
1034 -- check whether patch can be applied
1035 SELECT evergreen.upgrade_deps_block_check('0572', :eg_version);
1036
1037 CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT) RETURNS anyarray AS $$ SELECT ARRAY_ACCUM(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2; $$ LANGUAGE SQL;
1038
1039 CREATE TABLE vandelay.match_set (
1040     id      SERIAL  PRIMARY KEY,
1041     name    TEXT        NOT NULL,
1042     owner   INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE,
1043     mtype   TEXT        NOT NULL DEFAULT 'biblio', -- 'biblio','authority','mfhd'?, others?
1044     CONSTRAINT name_once_per_owner_mtype UNIQUE (name, owner, mtype)
1045 );
1046
1047 -- Table to define match points, either FF via SVF or tag+subfield
1048 CREATE TABLE vandelay.match_set_point (
1049     id          SERIAL  PRIMARY KEY,
1050     match_set   INT     REFERENCES vandelay.match_set (id) ON DELETE CASCADE,
1051     parent      INT     REFERENCES vandelay.match_set_point (id),
1052     bool_op     TEXT    CHECK (bool_op IS NULL OR (bool_op IN ('AND','OR','NOT'))),
1053     svf         TEXT    REFERENCES config.record_attr_definition (name),
1054     tag         TEXT,
1055     subfield    TEXT,
1056     negate      BOOL    DEFAULT FALSE,
1057     quality     INT     NOT NULL DEFAULT 1, -- higher is better
1058     CONSTRAINT vmsp_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL),
1059     CONSTRAINT vmsp_need_a_tag_or_a_ff_or_a_bo CHECK (
1060         (tag IS NOT NULL AND svf IS NULL AND bool_op IS NULL) OR
1061         (tag IS NULL AND svf IS NOT NULL AND bool_op IS NULL) OR
1062         (tag IS NULL AND svf IS NULL AND bool_op IS NOT NULL)
1063     )
1064 );
1065
1066 CREATE TABLE vandelay.match_set_quality (
1067     id          SERIAL  PRIMARY KEY,
1068     match_set   INT     NOT NULL REFERENCES vandelay.match_set (id) ON DELETE CASCADE,
1069     svf         TEXT    REFERENCES config.record_attr_definition,
1070     tag         TEXT,
1071     subfield    TEXT,
1072     value       TEXT    NOT NULL,
1073     quality     INT     NOT NULL DEFAULT 1, -- higher is better
1074     CONSTRAINT vmsq_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL),
1075     CONSTRAINT vmsq_need_a_tag_or_a_ff CHECK ((tag IS NOT NULL AND svf IS NULL) OR (tag IS NULL AND svf IS NOT NULL))
1076 );
1077 CREATE UNIQUE INDEX vmsq_def_once_per_set ON vandelay.match_set_quality (match_set, COALESCE(tag,''), COALESCE(subfield,''), COALESCE(svf,''), value);
1078
1079
1080 -- ALTER TABLEs...
1081 ALTER TABLE vandelay.queue ADD COLUMN match_set INT REFERENCES vandelay.match_set (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;
1082 ALTER TABLE vandelay.queued_record ADD COLUMN quality INT NOT NULL DEFAULT 0;
1083 ALTER TABLE vandelay.bib_attr_definition DROP COLUMN ident;
1084
1085 CREATE TABLE vandelay.import_error (
1086     code        TEXT    PRIMARY KEY,
1087     description TEXT    NOT NULL -- i18n
1088 );
1089
1090 ALTER TABLE vandelay.queued_bib_record
1091     ADD COLUMN import_error TEXT REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
1092     ADD COLUMN error_detail TEXT;
1093
1094 ALTER TABLE vandelay.bib_match
1095     DROP COLUMN field_type,
1096     DROP COLUMN matched_attr,
1097     ADD COLUMN quality INT NOT NULL DEFAULT 1,
1098     ADD COLUMN match_score INT NOT NULL DEFAULT 0;
1099
1100 ALTER TABLE vandelay.import_item
1101     ADD COLUMN import_error TEXT REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
1102     ADD COLUMN error_detail TEXT,
1103     ADD COLUMN imported_as BIGINT REFERENCES asset.copy (id) DEFERRABLE INITIALLY DEFERRED,
1104     ADD COLUMN import_time TIMESTAMP WITH TIME ZONE;
1105
1106 ALTER TABLE vandelay.merge_profile ADD COLUMN lwm_ratio NUMERIC;
1107
1108 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
1109 DECLARE
1110     ldr         TEXT;
1111     tval        TEXT;
1112     tval_rec    RECORD;
1113     bval        TEXT;
1114     bval_rec    RECORD;
1115     retval      config.marc21_rec_type_map%ROWTYPE;
1116 BEGIN
1117     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
1118
1119     IF ldr IS NULL OR ldr = '' THEN
1120         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1121         RETURN retval;
1122     END IF;
1123
1124     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
1125     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
1126
1127
1128     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1129     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1130
1131     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1132
1133     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1134
1135
1136     IF retval.code IS NULL THEN
1137         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1138     END IF;
1139
1140     RETURN retval;
1141 END;
1142 $func$ LANGUAGE PLPGSQL;
1143
1144 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
1145 DECLARE
1146     rtype       TEXT;
1147     ff_pos      RECORD;
1148     tag_data    RECORD;
1149     val         TEXT;
1150 BEGIN
1151     rtype := (vandelay.marc21_record_type( marc )).code;
1152     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
1153         IF ff_pos.tag = 'ldr' THEN
1154             val := oils_xpath_string('//*[local-name()="leader"]', marc);
1155             IF val IS NOT NULL THEN
1156                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
1157                 RETURN val;
1158             END IF;
1159         ELSE
1160             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1161                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1162                 RETURN val;
1163             END LOOP;
1164         END IF;
1165         val := REPEAT( ff_pos.default_val, ff_pos.length );
1166         RETURN val;
1167     END LOOP;
1168
1169     RETURN NULL;
1170 END;
1171 $func$ LANGUAGE PLPGSQL;
1172
1173 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
1174 DECLARE
1175     tag_data    TEXT;
1176     rtype       TEXT;
1177     ff_pos      RECORD;
1178     output      biblio.record_ff_map%ROWTYPE;
1179 BEGIN
1180     rtype := (vandelay.marc21_record_type( marc )).code;
1181
1182     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
1183         output.ff_name  := ff_pos.fixed_field;
1184         output.ff_value := NULL;
1185
1186         IF ff_pos.tag = 'ldr' THEN
1187             output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
1188             IF output.ff_value IS NOT NULL THEN
1189                 output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
1190                 RETURN NEXT output;
1191                 output.ff_value := NULL;
1192             END IF;
1193         ELSE
1194             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1195                 output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
1196                 IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
1197                 RETURN NEXT output;
1198                 output.ff_value := NULL;
1199             END LOOP;
1200         END IF;
1201     
1202     END LOOP;
1203
1204     RETURN;
1205 END;
1206 $func$ LANGUAGE PLPGSQL;
1207
1208 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1209 DECLARE
1210     rowid   INT := 0;
1211     _007    TEXT;
1212     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
1213     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
1214     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
1215     retval  biblio.marc21_physical_characteristics%ROWTYPE;
1216 BEGIN
1217
1218     _007 := oils_xpath_string( '//*[@tag="007"]', marc );
1219
1220     IF _007 IS NOT NULL AND _007 <> '' THEN
1221         SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
1222
1223         IF ptype.ptype_key IS NOT NULL THEN
1224             FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
1225                 SELECT * INTO pval FROM config.marc21_physical_characteristic_value_map WHERE ptype_subfield = psf.id AND value = SUBSTRING( _007, psf.start_pos + 1, psf.length );
1226
1227                 IF pval.id IS NOT NULL THEN
1228                     rowid := rowid + 1;
1229                     retval.id := rowid;
1230                     retval.ptype := ptype.ptype_key;
1231                     retval.subfield := psf.id;
1232                     retval.value := pval.id;
1233                     RETURN NEXT retval;
1234                 END IF;
1235
1236             END LOOP;
1237         END IF;
1238     END IF;
1239
1240     RETURN;
1241 END;
1242 $func$ LANGUAGE PLPGSQL;
1243
1244 CREATE TYPE vandelay.flat_marc AS ( tag CHAR(3), ind1 TEXT, ind2 TEXT, subfield TEXT, value TEXT );
1245 CREATE OR REPLACE FUNCTION vandelay.flay_marc ( TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
1246
1247 use MARC::Record;
1248 use MARC::File::XML (BinaryEncoding => 'UTF-8');
1249 use MARC::Charset;
1250 use strict;
1251
1252 MARC::Charset->assume_unicode(1);
1253
1254 my $xml = shift;
1255 my $r = MARC::Record->new_from_xml( $xml );
1256
1257 return_next( { tag => 'LDR', value => $r->leader } );
1258
1259 for my $f ( $r->fields ) {
1260     if ($f->is_control_field) {
1261         return_next({ tag => $f->tag, value => $f->data });
1262     } else {
1263         for my $s ($f->subfields) {
1264             return_next({
1265                 tag      => $f->tag,
1266                 ind1     => $f->indicator(1),
1267                 ind2     => $f->indicator(2),
1268                 subfield => $s->[0],
1269                 value    => $s->[1]
1270             });
1271
1272             if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
1273                 my $trim = $f->indicator(2) || 0;
1274                 return_next({
1275                     tag      => 'tnf',
1276                     ind1     => $f->indicator(1),
1277                     ind2     => $f->indicator(2),
1278                     subfield => 'a',
1279                     value    => substr( $s->[1], $trim )
1280                 });
1281             }
1282         }
1283     }
1284 }
1285
1286 return undef;
1287
1288 $func$ LANGUAGE PLPERLU;
1289
1290 CREATE OR REPLACE FUNCTION vandelay.flatten_marc ( marc TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
1291 DECLARE
1292     output  vandelay.flat_marc%ROWTYPE;
1293     field   RECORD;
1294 BEGIN
1295     FOR field IN SELECT * FROM vandelay.flay_marc( marc ) LOOP
1296         output.ind1 := field.ind1;
1297         output.ind2 := field.ind2;
1298         output.tag := field.tag;
1299         output.subfield := field.subfield;
1300         IF field.subfield IS NOT NULL AND field.tag NOT IN ('020','022','024') THEN -- exclude standard numbers and control fields
1301             output.value := naco_normalize(field.value, field.subfield);
1302         ELSE
1303             output.value := field.value;
1304         END IF;
1305
1306         CONTINUE WHEN output.value IS NULL;
1307
1308         RETURN NEXT output;
1309     END LOOP;
1310 END;
1311 $func$ LANGUAGE PLPGSQL;
1312
1313 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT, attr_defs TEXT[]) RETURNS hstore AS $_$
1314 DECLARE
1315     transformed_xml TEXT;
1316     prev_xfrm       TEXT;
1317     normalizer      RECORD;
1318     xfrm            config.xml_transform%ROWTYPE;
1319     attr_value      TEXT;
1320     new_attrs       HSTORE := ''::HSTORE;
1321     attr_def        config.record_attr_definition%ROWTYPE;
1322 BEGIN
1323
1324     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE name IN (SELECT * FROM UNNEST(attr_defs)) ORDER BY format LOOP
1325
1326         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1327             SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(x.value), COALESCE(attr_def.joiner,' ')) INTO attr_value
1328               FROM  vandelay.flatten_marc(xml) AS x
1329               WHERE x.tag LIKE attr_def.tag
1330                     AND CASE
1331                         WHEN attr_def.sf_list IS NOT NULL
1332                             THEN POSITION(x.subfield IN attr_def.sf_list) > 0
1333                         ELSE TRUE
1334                         END
1335               GROUP BY x.tag
1336               ORDER BY x.tag
1337               LIMIT 1;
1338
1339         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1340             attr_value := vandelay.marc21_extract_fixed_field(xml, attr_def.fixed_field);
1341
1342         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1343
1344             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1345
1346             -- See if we can skip the XSLT ... it's expensive
1347             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1348                 -- Can't skip the transform
1349                 IF xfrm.xslt <> '---' THEN
1350                     transformed_xml := oils_xslt_process(xml,xfrm.xslt);
1351                 ELSE
1352                     transformed_xml := xml;
1353                 END IF;
1354
1355                 prev_xfrm := xfrm.name;
1356             END IF;
1357
1358             IF xfrm.name IS NULL THEN
1359                 -- just grab the marcxml (empty) transform
1360                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1361                 prev_xfrm := xfrm.name;
1362             END IF;
1363
1364             attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
1365
1366         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1367             SELECT  m.value::TEXT INTO attr_value
1368               FROM  vandelay.marc21_physical_characteristics(xml) v
1369                     JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
1370               WHERE v.subfield = attr_def.phys_char_sf
1371               LIMIT 1; -- Just in case ...
1372
1373         END IF;
1374
1375         -- apply index normalizers to attr_value
1376         FOR normalizer IN
1377             SELECT  n.func AS func,
1378                     n.param_count AS param_count,
1379                     m.params AS params
1380               FROM  config.index_normalizer n
1381                     JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1382               WHERE attr = attr_def.name
1383               ORDER BY m.pos LOOP
1384                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
1385                     quote_literal( attr_value ) ||
1386                     CASE
1387                         WHEN normalizer.param_count > 0
1388                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1389                             ELSE ''
1390                         END ||
1391                     ')' INTO attr_value;
1392
1393         END LOOP;
1394
1395         -- Add the new value to the hstore
1396         new_attrs := new_attrs || hstore( attr_def.name, attr_value );
1397
1398     END LOOP;
1399
1400     RETURN new_attrs;
1401 END;
1402 $_$ LANGUAGE PLPGSQL;
1403
1404 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT ) RETURNS hstore AS $_$
1405     SELECT vandelay.extract_rec_attrs( $1, (SELECT ARRAY_ACCUM(name) FROM config.record_attr_definition));
1406 $_$ LANGUAGE SQL;
1407
1408 -- Everything between this comment and the beginning of the definition of
1409 -- vandelay.match_bib_record() is strictly in service of that function.
1410 CREATE TYPE vandelay.match_set_test_result AS (record BIGINT, quality INTEGER);
1411
1412 CREATE OR REPLACE FUNCTION vandelay.match_set_test_marcxml(
1413     match_set_id INTEGER, record_xml TEXT
1414 ) RETURNS SETOF vandelay.match_set_test_result AS $$
1415 DECLARE
1416     tags_rstore HSTORE;
1417     svf_rstore  HSTORE;
1418     coal        TEXT;
1419     joins       TEXT;
1420     query_      TEXT;
1421     wq          TEXT;
1422     qvalue      INTEGER;
1423     rec         RECORD;
1424 BEGIN
1425     tags_rstore := vandelay.flatten_marc_hstore(record_xml);
1426     svf_rstore := vandelay.extract_rec_attrs(record_xml);
1427
1428     CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
1429     CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
1430
1431     -- generate the where clause and return that directly (into wq), and as
1432     -- a side-effect, populate the _vandelay_tmp_[qj]rows tables.
1433     wq := vandelay.get_expr_from_match_set(match_set_id);
1434
1435     query_ := 'SELECT bre.id AS record, ';
1436
1437     -- qrows table is for the quality bits we add to the SELECT clause
1438     SELECT ARRAY_TO_STRING(
1439         ARRAY_ACCUM('COALESCE(n' || q::TEXT || '.quality, 0)'), ' + '
1440     ) INTO coal FROM _vandelay_tmp_qrows;
1441
1442     -- our query string so far is the SELECT clause and the inital FROM.
1443     -- no JOINs yet nor the WHERE clause
1444     query_ := query_ || coal || ' AS quality ' || E'\n' ||
1445         'FROM biblio.record_entry bre ';
1446
1447     -- jrows table is for the joins we must make (and the real text conditions)
1448     SELECT ARRAY_TO_STRING(ARRAY_ACCUM(j), E'\n') INTO joins
1449         FROM _vandelay_tmp_jrows;
1450
1451     -- add those joins and the where clause to our query.
1452     query_ := query_ || joins || E'\n' || 'WHERE ' || wq || ' AND not bre.deleted';
1453
1454     -- this will return rows of record,quality
1455     FOR rec IN EXECUTE query_ USING tags_rstore, svf_rstore LOOP
1456         RETURN NEXT rec;
1457     END LOOP;
1458
1459     DROP TABLE _vandelay_tmp_qrows;
1460     DROP TABLE _vandelay_tmp_jrows;
1461     RETURN;
1462 END;
1463
1464 $$ LANGUAGE PLPGSQL;
1465
1466 CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
1467     record_xml TEXT
1468 ) RETURNS HSTORE AS $$
1469 BEGIN
1470     RETURN (SELECT
1471         HSTORE(
1472             ARRAY_ACCUM(tag || (COALESCE(subfield, ''))),
1473             ARRAY_ACCUM(value)
1474         )
1475         FROM (
1476             SELECT tag, subfield, ARRAY_ACCUM(value)::TEXT AS value
1477                 FROM vandelay.flatten_marc(record_xml)
1478                 GROUP BY tag, subfield ORDER BY tag, subfield
1479         ) subquery
1480     );
1481 END;
1482 $$ LANGUAGE PLPGSQL;
1483
1484 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set(
1485     match_set_id INTEGER
1486 ) RETURNS TEXT AS $$
1487 DECLARE
1488     root    vandelay.match_set_point;
1489 BEGIN
1490     SELECT * INTO root FROM vandelay.match_set_point
1491         WHERE parent IS NULL AND match_set = match_set_id;
1492
1493     RETURN vandelay.get_expr_from_match_set_point(root);
1494 END;
1495 $$  LANGUAGE PLPGSQL;
1496
1497 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set_point(
1498     node vandelay.match_set_point
1499 ) RETURNS TEXT AS $$
1500 DECLARE
1501     q           TEXT;
1502     i           INTEGER;
1503     this_op     TEXT;
1504     children    INTEGER[];
1505     child       vandelay.match_set_point;
1506 BEGIN
1507     SELECT ARRAY_ACCUM(id) INTO children FROM vandelay.match_set_point
1508         WHERE parent = node.id;
1509
1510     IF ARRAY_LENGTH(children, 1) > 0 THEN
1511         this_op := vandelay._get_expr_render_one(node);
1512         q := '(';
1513         i := 1;
1514         WHILE children[i] IS NOT NULL LOOP
1515             SELECT * INTO child FROM vandelay.match_set_point
1516                 WHERE id = children[i];
1517             IF i > 1 THEN
1518                 q := q || ' ' || this_op || ' ';
1519             END IF;
1520             i := i + 1;
1521             q := q || vandelay.get_expr_from_match_set_point(child);
1522         END LOOP;
1523         q := q || ')';
1524         RETURN q;
1525     ELSIF node.bool_op IS NULL THEN
1526         PERFORM vandelay._get_expr_push_qrow(node);
1527         PERFORM vandelay._get_expr_push_jrow(node);
1528         RETURN vandelay._get_expr_render_one(node);
1529     ELSE
1530         RETURN '';
1531     END IF;
1532 END;
1533 $$  LANGUAGE PLPGSQL;
1534
1535 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_qrow(
1536     node vandelay.match_set_point
1537 ) RETURNS VOID AS $$
1538 DECLARE
1539 BEGIN
1540     INSERT INTO _vandelay_tmp_qrows (q) VALUES (node.id);
1541 END;
1542 $$ LANGUAGE PLPGSQL;
1543
1544 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_jrow(
1545     node vandelay.match_set_point
1546 ) RETURNS VOID AS $$
1547 DECLARE
1548     jrow        TEXT;
1549     my_alias    TEXT;
1550     op          TEXT;
1551     tagkey      TEXT;
1552 BEGIN
1553     IF node.negate THEN
1554         op := '<>';
1555     ELSE
1556         op := '=';
1557     END IF;
1558
1559     IF node.tag IS NOT NULL THEN
1560         tagkey := node.tag;
1561         IF node.subfield IS NOT NULL THEN
1562             tagkey := tagkey || node.subfield;
1563         END IF;
1564     END IF;
1565
1566     my_alias := 'n' || node.id::TEXT;
1567
1568     jrow := 'LEFT JOIN (SELECT *, ' || node.quality ||
1569         ' AS quality FROM metabib.';
1570     IF node.tag IS NOT NULL THEN
1571         jrow := jrow || 'full_rec) ' || my_alias || ' ON (' ||
1572             my_alias || '.record = bre.id AND ' || my_alias || '.tag = ''' ||
1573             node.tag || '''';
1574         IF node.subfield IS NOT NULL THEN
1575             jrow := jrow || ' AND ' || my_alias || '.subfield = ''' ||
1576                 node.subfield || '''';
1577         END IF;
1578         jrow := jrow || ' AND (' || my_alias || '.value ' || op ||
1579             ' ANY(($1->''' || tagkey || ''')::TEXT[])))';
1580     ELSE    -- svf
1581         jrow := jrow || 'record_attr) ' || my_alias || ' ON (' ||
1582             my_alias || '.id = bre.id AND (' ||
1583             my_alias || '.attrs->''' || node.svf ||
1584             ''' ' || op || ' $2->''' || node.svf || '''))';
1585     END IF;
1586     INSERT INTO _vandelay_tmp_jrows (j) VALUES (jrow);
1587 END;
1588 $$ LANGUAGE PLPGSQL;
1589
1590 CREATE OR REPLACE FUNCTION vandelay._get_expr_render_one(
1591     node vandelay.match_set_point
1592 ) RETURNS TEXT AS $$
1593 DECLARE
1594     s           TEXT;
1595 BEGIN
1596     IF node.bool_op IS NOT NULL THEN
1597         RETURN node.bool_op;
1598     ELSE
1599         RETURN '(n' || node.id::TEXT || '.id IS NOT NULL)';
1600     END IF;
1601 END;
1602 $$ LANGUAGE PLPGSQL;
1603
1604 CREATE OR REPLACE FUNCTION vandelay.match_bib_record() RETURNS TRIGGER AS $func$
1605 DECLARE
1606     incoming_existing_id    TEXT;
1607     test_result             vandelay.match_set_test_result%ROWTYPE;
1608     tmp_rec                 BIGINT;
1609     match_set               INT;
1610 BEGIN
1611     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1612         RETURN NEW;
1613     END IF;
1614
1615     DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
1616
1617     SELECT q.match_set INTO match_set FROM vandelay.bib_queue q WHERE q.id = NEW.queue;
1618
1619     IF match_set IS NOT NULL THEN
1620         NEW.quality := vandelay.measure_record_quality( NEW.marc, match_set );
1621     END IF;
1622
1623     -- Perfect matches on 901$c exit early with a match with high quality.
1624     incoming_existing_id :=
1625         oils_xpath_string('//*[@tag="901"]/*[@code="c"][1]', NEW.marc);
1626
1627     IF incoming_existing_id IS NOT NULL AND incoming_existing_id != '' THEN
1628         SELECT id INTO tmp_rec FROM biblio.record_entry WHERE id = incoming_existing_id::bigint;
1629         IF tmp_rec IS NOT NULL THEN
1630             INSERT INTO vandelay.bib_match (queued_record, eg_record, match_score, quality) 
1631                 SELECT
1632                     NEW.id, 
1633                     b.id,
1634                     9999,
1635                     -- note: no match_set means quality==0
1636                     vandelay.measure_record_quality( b.marc, match_set )
1637                 FROM biblio.record_entry b
1638                 WHERE id = incoming_existing_id::bigint;
1639         END IF;
1640     END IF;
1641
1642     IF match_set IS NULL THEN
1643         RETURN NEW;
1644     END IF;
1645
1646     FOR test_result IN SELECT * FROM
1647         vandelay.match_set_test_marcxml(match_set, NEW.marc) LOOP
1648
1649         INSERT INTO vandelay.bib_match ( queued_record, eg_record, match_score, quality )
1650             SELECT  
1651                 NEW.id,
1652                 test_result.record,
1653                 test_result.quality,
1654                 vandelay.measure_record_quality( b.marc, match_set )
1655                 FROM  biblio.record_entry b
1656                 WHERE id = test_result.record;
1657
1658     END LOOP;
1659
1660     RETURN NEW;
1661 END;
1662 $func$ LANGUAGE PLPGSQL;
1663
1664 CREATE OR REPLACE FUNCTION vandelay.measure_record_quality ( xml TEXT, match_set_id INT ) RETURNS INT AS $_$
1665 DECLARE
1666     out_q   INT := 0;
1667     rvalue  TEXT;
1668     test    vandelay.match_set_quality%ROWTYPE;
1669 BEGIN
1670
1671     FOR test IN SELECT * FROM vandelay.match_set_quality WHERE match_set = match_set_id LOOP
1672         IF test.tag IS NOT NULL THEN
1673             FOR rvalue IN SELECT value FROM vandelay.flatten_marc( xml ) WHERE tag = test.tag AND subfield = test.subfield LOOP
1674                 IF test.value = rvalue THEN
1675                     out_q := out_q + test.quality;
1676                 END IF;
1677             END LOOP;
1678         ELSE
1679             IF test.value = vandelay.extract_rec_attrs(xml, ARRAY[test.svf]) -> test.svf THEN
1680                 out_q := out_q + test.quality;
1681             END IF;
1682         END IF;
1683     END LOOP;
1684
1685     RETURN out_q;
1686 END;
1687 $_$ LANGUAGE PLPGSQL;
1688
1689
1690 CREATE OR REPLACE FUNCTION vandelay.overlay_bib_record ( import_id BIGINT, eg_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1691 DECLARE
1692     merge_profile   vandelay.merge_profile%ROWTYPE;
1693     dyn_profile     vandelay.compile_profile%ROWTYPE;
1694     editor_string   TEXT;
1695     editor_id       INT;
1696     source_marc     TEXT;
1697     target_marc     TEXT;
1698     eg_marc         TEXT;
1699     v_marc          TEXT;
1700     replace_rule    TEXT;
1701 BEGIN
1702
1703     SELECT  q.marc INTO v_marc
1704       FROM  vandelay.queued_record q
1705             JOIN vandelay.bib_match m ON (m.queued_record = q.id AND q.id = import_id)
1706       LIMIT 1;
1707
1708     IF v_marc IS NULL THEN
1709         -- RAISE NOTICE 'no marc for vandelay or bib record';
1710         RETURN FALSE;
1711     END IF;
1712
1713     IF vandelay.template_overlay_bib_record( v_marc, eg_id, merge_profile_id) THEN
1714         UPDATE  vandelay.queued_bib_record
1715           SET   imported_as = eg_id,
1716                 import_time = NOW()
1717           WHERE id = import_id;
1718
1719         editor_string := (oils_xpath('//*[@tag="905"]/*[@code="u"]/text()',v_marc))[1];
1720
1721         IF editor_string IS NOT NULL AND editor_string <> '' THEN
1722             SELECT usr INTO editor_id FROM actor.card WHERE barcode = editor_string;
1723
1724             IF editor_id IS NULL THEN
1725                 SELECT id INTO editor_id FROM actor.usr WHERE usrname = editor_string;
1726             END IF;
1727
1728             IF editor_id IS NOT NULL THEN
1729                 UPDATE biblio.record_entry SET editor = editor_id WHERE id = eg_id;
1730             END IF;
1731         END IF;
1732
1733         RETURN TRUE;
1734     END IF;
1735
1736     -- RAISE NOTICE 'update of biblio.record_entry failed';
1737
1738     RETURN FALSE;
1739
1740 END;
1741 $$ LANGUAGE PLPGSQL;
1742
1743
1744 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record_with_best ( import_id BIGINT, merge_profile_id INT, lwm_ratio_value_p NUMERIC ) RETURNS BOOL AS $$
1745 DECLARE
1746     eg_id           BIGINT;
1747     lwm_ratio_value NUMERIC;
1748 BEGIN
1749
1750     lwm_ratio_value := COALESCE(lwm_ratio_value_p, 0.0);
1751
1752     PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
1753
1754     IF FOUND THEN
1755         -- RAISE NOTICE 'already imported, cannot auto-overlay'
1756         RETURN FALSE;
1757     END IF;
1758
1759     SELECT  m.eg_record INTO eg_id
1760       FROM  vandelay.bib_match m
1761             JOIN vandelay.queued_bib_record qr ON (m.queued_record = qr.id)
1762             JOIN vandelay.bib_queue q ON (qr.queue = q.id)
1763             JOIN biblio.record_entry r ON (r.id = m.eg_record)
1764       WHERE m.queued_record = import_id
1765             AND qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC >= lwm_ratio_value
1766       ORDER BY  m.match_score DESC, -- required match score
1767                 qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC DESC, -- quality tie breaker
1768                 m.id -- when in doubt, use the first match
1769       LIMIT 1;
1770
1771     IF eg_id IS NULL THEN
1772         -- RAISE NOTICE 'incoming record is not of high enough quality';
1773         RETURN FALSE;
1774     END IF;
1775
1776     RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
1777 END;
1778 $$ LANGUAGE PLPGSQL;
1779
1780 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record_with_best ( import_id BIGINT, merge_profile_id INT, lwm_ratio_value_p NUMERIC ) RETURNS BOOL AS $$
1781 DECLARE
1782     eg_id           BIGINT;
1783     lwm_ratio_value NUMERIC;
1784 BEGIN
1785
1786     lwm_ratio_value := COALESCE(lwm_ratio_value_p, 0.0);
1787
1788     PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
1789
1790     IF FOUND THEN
1791         -- RAISE NOTICE 'already imported, cannot auto-overlay'
1792         RETURN FALSE;
1793     END IF;
1794
1795     SELECT  m.eg_record INTO eg_id
1796       FROM  vandelay.bib_match m
1797             JOIN vandelay.queued_bib_record qr ON (m.queued_record = qr.id)
1798             JOIN vandelay.bib_queue q ON (qr.queue = q.id)
1799             JOIN biblio.record_entry r ON (r.id = m.eg_record)
1800       WHERE m.queued_record = import_id
1801             AND qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC >= lwm_ratio_value
1802       ORDER BY  m.match_score DESC, -- required match score
1803                 qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC DESC, -- quality tie breaker
1804                 m.id -- when in doubt, use the first match
1805       LIMIT 1;
1806
1807     IF eg_id IS NULL THEN
1808         -- RAISE NOTICE 'incoming record is not of high enough quality';
1809         RETURN FALSE;
1810     END IF;
1811
1812     RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
1813 END;
1814 $$ LANGUAGE PLPGSQL;
1815
1816
1817 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue_with_best ( queue_id BIGINT, merge_profile_id INT, lwm_ratio_value NUMERIC ) RETURNS SETOF BIGINT AS $$
1818 DECLARE
1819     queued_record   vandelay.queued_bib_record%ROWTYPE;
1820 BEGIN
1821
1822     FOR queued_record IN SELECT * FROM vandelay.queued_bib_record WHERE queue = queue_id AND import_time IS NULL LOOP
1823
1824         IF vandelay.auto_overlay_bib_record_with_best( queued_record.id, merge_profile_id, lwm_ratio_value ) THEN
1825             RETURN NEXT queued_record.id;
1826         END IF;
1827
1828     END LOOP;
1829
1830     RETURN;
1831     
1832 END;
1833 $$ LANGUAGE PLPGSQL;
1834
1835 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue_with_best ( import_id BIGINT, merge_profile_id INT ) RETURNS SETOF BIGINT AS $$
1836     SELECT vandelay.auto_overlay_bib_queue_with_best( $1, $2, p.lwm_ratio ) FROM vandelay.merge_profile p WHERE id = $2;
1837 $$ LANGUAGE SQL;
1838
1839 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_marc ( ) RETURNS TRIGGER AS $$
1840 DECLARE
1841     value   TEXT;
1842     atype   TEXT;
1843     adef    RECORD;
1844 BEGIN
1845     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1846         RETURN NEW;
1847     END IF;
1848
1849     FOR adef IN SELECT * FROM vandelay.bib_attr_definition LOOP
1850
1851         SELECT extract_marc_field('vandelay.queued_bib_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_bib_record WHERE id = NEW.id;
1852         IF (value IS NOT NULL AND value <> '') THEN
1853             INSERT INTO vandelay.queued_bib_record_attr (record, field, attr_value) VALUES (NEW.id, adef.id, value);
1854         END IF;
1855
1856     END LOOP;
1857
1858     RETURN NULL;
1859 END;
1860 $$ LANGUAGE PLPGSQL;
1861
1862 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_items ( ) RETURNS TRIGGER AS $func$
1863 DECLARE
1864     attr_def    BIGINT;
1865     item_data   vandelay.import_item%ROWTYPE;
1866 BEGIN
1867
1868     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1869         RETURN NEW;
1870     END IF;
1871
1872     SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
1873
1874     FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
1875         INSERT INTO vandelay.import_item (
1876             record,
1877             definition,
1878             owning_lib,
1879             circ_lib,
1880             call_number,
1881             copy_number,
1882             status,
1883             location,
1884             circulate,
1885             deposit,
1886             deposit_amount,
1887             ref,
1888             holdable,
1889             price,
1890             barcode,
1891             circ_modifier,
1892             circ_as_type,
1893             alert_message,
1894             pub_note,
1895             priv_note,
1896             opac_visible
1897         ) VALUES (
1898             NEW.id,
1899             item_data.definition,
1900             item_data.owning_lib,
1901             item_data.circ_lib,
1902             item_data.call_number,
1903             item_data.copy_number,
1904             item_data.status,
1905             item_data.location,
1906             item_data.circulate,
1907             item_data.deposit,
1908             item_data.deposit_amount,
1909             item_data.ref,
1910             item_data.holdable,
1911             item_data.price,
1912             item_data.barcode,
1913             item_data.circ_modifier,
1914             item_data.circ_as_type,
1915             item_data.alert_message,
1916             item_data.pub_note,
1917             item_data.priv_note,
1918             item_data.opac_visible
1919         );
1920     END LOOP;
1921
1922     RETURN NULL;
1923 END;
1924 $func$ LANGUAGE PLPGSQL;
1925
1926 CREATE OR REPLACE FUNCTION vandelay.cleanup_bib_marc ( ) RETURNS TRIGGER AS $$
1927 BEGIN
1928     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1929         RETURN NEW;
1930     END IF;
1931
1932     DELETE FROM vandelay.queued_bib_record_attr WHERE record = OLD.id;
1933     DELETE FROM vandelay.import_item WHERE record = OLD.id;
1934
1935     IF TG_OP = 'UPDATE' THEN
1936         RETURN NEW;
1937     END IF;
1938     RETURN OLD;
1939 END;
1940 $$ LANGUAGE PLPGSQL;
1941
1942 -- ALTER TABLEs...
1943
1944 DROP TRIGGER zz_match_bibs_trigger ON vandelay.queued_bib_record;
1945 CREATE TRIGGER zz_match_bibs_trigger
1946     BEFORE INSERT OR UPDATE ON vandelay.queued_bib_record
1947     FOR EACH ROW EXECUTE PROCEDURE vandelay.match_bib_record();
1948
1949 CREATE OR REPLACE FUNCTION vandelay.ingest_authority_marc ( ) RETURNS TRIGGER AS $$
1950 DECLARE
1951     value   TEXT;
1952     atype   TEXT;
1953     adef    RECORD;
1954 BEGIN
1955     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1956         RETURN NEW;
1957     END IF;
1958
1959     FOR adef IN SELECT * FROM vandelay.authority_attr_definition LOOP
1960
1961         SELECT extract_marc_field('vandelay.queued_authority_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_authority_record WHERE id = NEW.id;
1962         IF (value IS NOT NULL AND value <> '') THEN
1963             INSERT INTO vandelay.queued_authority_record_attr (record, field, attr_value) VALUES (NEW.id, adef.id, value);
1964         END IF;
1965
1966     END LOOP;
1967
1968     RETURN NULL;
1969 END;
1970 $$ LANGUAGE PLPGSQL;
1971
1972 ALTER TABLE vandelay.authority_attr_definition DROP COLUMN ident;
1973 ALTER TABLE vandelay.queued_authority_record
1974     ADD COLUMN import_error TEXT REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
1975     ADD COLUMN error_detail TEXT;
1976
1977 ALTER TABLE vandelay.authority_match DROP COLUMN matched_attr;
1978
1979 CREATE OR REPLACE FUNCTION vandelay.cleanup_authority_marc ( ) RETURNS TRIGGER AS $$
1980 BEGIN
1981     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1982         RETURN NEW;
1983     END IF;
1984
1985     DELETE FROM vandelay.queued_authority_record_attr WHERE record = OLD.id;
1986     IF TG_OP = 'UPDATE' THEN
1987         RETURN NEW;
1988     END IF;
1989     RETURN OLD;
1990 END;
1991 $$ LANGUAGE PLPGSQL;
1992
1993 CREATE OR REPLACE FUNCTION authority.flatten_marc ( rid BIGINT ) RETURNS SETOF authority.full_rec AS $func$
1994 DECLARE
1995         auth    authority.record_entry%ROWTYPE;
1996         output  authority.full_rec%ROWTYPE;
1997         field   RECORD;
1998 BEGIN
1999         SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
2000
2001         FOR field IN SELECT * FROM vandelay.flatten_marc( auth.marc ) LOOP
2002                 output.record := rid;
2003                 output.ind1 := field.ind1;
2004                 output.ind2 := field.ind2;
2005                 output.tag := field.tag;
2006                 output.subfield := field.subfield;
2007                 output.value := field.value;
2008
2009                 RETURN NEXT output;
2010         END LOOP;
2011 END;
2012 $func$ LANGUAGE PLPGSQL;
2013
2014 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
2015 DECLARE
2016         bib     biblio.record_entry%ROWTYPE;
2017         output  metabib.full_rec%ROWTYPE;
2018         field   RECORD;
2019 BEGIN
2020         SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
2021
2022         FOR field IN SELECT * FROM vandelay.flatten_marc( bib.marc ) LOOP
2023                 output.record := rid;
2024                 output.ind1 := field.ind1;
2025                 output.ind2 := field.ind2;
2026                 output.tag := field.tag;
2027                 output.subfield := field.subfield;
2028                 output.value := field.value;
2029
2030                 RETURN NEXT output;
2031         END LOOP;
2032 END;
2033 $func$ LANGUAGE PLPGSQL;
2034
2035 -----------------------------------------------
2036 -- Seed data for import errors
2037 -----------------------------------------------
2038
2039 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'general.unknown', oils_i18n_gettext('general.unknown', 'Import or Overlay failed', 'vie', 'description') );
2040 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.item.duplicate.barcode', oils_i18n_gettext('import.item.duplicate.barcode', 'Import failed due to barcode collision', 'vie', 'description') );
2041 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.item.invalid.circ_modifier', oils_i18n_gettext('import.item.invalid.circ_modifier', 'Import failed due to invalid circulation modifier', 'vie', 'description') );
2042 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.item.invalid.location', oils_i18n_gettext('import.item.invalid.location', 'Import failed due to invalid copy location', 'vie', 'description') );
2043 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.duplicate.sysid', oils_i18n_gettext('import.duplicate.sysid', 'Import failed due to system id collision', 'vie', 'description') );
2044 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.duplicate.tcn', oils_i18n_gettext('import.duplicate.sysid', 'Import failed due to system id collision', 'vie', 'description') );
2045 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'overlay.missing.sysid', oils_i18n_gettext('overlay.missing.sysid', 'Overlay failed due to missing system id', 'vie', 'description') );
2046 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.auth.duplicate.acn', oils_i18n_gettext('import.auth.duplicate.acn', 'Import failed due to Accession Number collision', 'vie', 'description') );
2047 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.xml.malformed', oils_i18n_gettext('import.xml.malformed', 'Malformed record cause Import failure', 'vie', 'description') );
2048 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'overlay.xml.malformed', oils_i18n_gettext('overlay.xml.malformed', 'Malformed record cause Overlay failure', 'vie', 'description') );
2049 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'overlay.record.quality', oils_i18n_gettext('overlay.record.quality', 'New record had insufficient quality', 'vie', 'description') );
2050
2051
2052 ----------------------------------------------------------------
2053 -- Seed data for queued record/item exports
2054 ----------------------------------------------------------------
2055
2056 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES (
2057         'vandelay.queued_bib_record.print',
2058         'vqbr', 
2059         oils_i18n_gettext(
2060             'vandelay.queued_bib_record.print',
2061             'Print output has been requested for records in an Importer Bib Queue.',
2062             'ath',
2063             'description'
2064         ), 
2065         FALSE
2066     )
2067     ,(
2068         'vandelay.queued_bib_record.csv',
2069         'vqbr', 
2070         oils_i18n_gettext(
2071             'vandelay.queued_bib_record.csv',
2072             'CSV output has been requested for records in an Importer Bib Queue.',
2073             'ath',
2074             'description'
2075         ), 
2076         FALSE
2077     )
2078     ,(
2079         'vandelay.queued_bib_record.email',
2080         'vqbr', 
2081         oils_i18n_gettext(
2082             'vandelay.queued_bib_record.email',
2083             'An email has been requested for records in an Importer Bib Queue.',
2084             'ath',
2085             'description'
2086         ), 
2087         FALSE
2088     )
2089     ,(
2090         'vandelay.queued_auth_record.print',
2091         'vqar', 
2092         oils_i18n_gettext(
2093             'vandelay.queued_auth_record.print',
2094             'Print output has been requested for records in an Importer Authority Queue.',
2095             'ath',
2096             'description'
2097         ), 
2098         FALSE
2099     )
2100     ,(
2101         'vandelay.queued_auth_record.csv',
2102         'vqar', 
2103         oils_i18n_gettext(
2104             'vandelay.queued_auth_record.csv',
2105             'CSV output has been requested for records in an Importer Authority Queue.',
2106             'ath',
2107             'description'
2108         ), 
2109         FALSE
2110     )
2111     ,(
2112         'vandelay.queued_auth_record.email',
2113         'vqar', 
2114         oils_i18n_gettext(
2115             'vandelay.queued_auth_record.email',
2116             'An email has been requested for records in an Importer Authority Queue.',
2117             'ath',
2118             'description'
2119         ), 
2120         FALSE
2121     )
2122     ,(
2123         'vandelay.import_items.print',
2124         'vii', 
2125         oils_i18n_gettext(
2126             'vandelay.import_items.print',
2127             'Print output has been requested for Import Items from records in an Importer Bib Queue.',
2128             'ath',
2129             'description'
2130         ), 
2131         FALSE
2132     )
2133     ,(
2134         'vandelay.import_items.csv',
2135         'vii', 
2136         oils_i18n_gettext(
2137             'vandelay.import_items.csv',
2138             'CSV output has been requested for Import Items from records in an Importer Bib Queue.',
2139             'ath',
2140             'description'
2141         ), 
2142         FALSE
2143     )
2144     ,(
2145         'vandelay.import_items.email',
2146         'vii', 
2147         oils_i18n_gettext(
2148             'vandelay.import_items.email',
2149             'An email has been requested for Import Items from records in an Importer Bib Queue.',
2150             'ath',
2151             'description'
2152         ), 
2153         FALSE
2154     )
2155 ;
2156
2157 INSERT INTO action_trigger.event_definition (
2158         id,
2159         active,
2160         owner,
2161         name,
2162         hook,
2163         validator,
2164         reactor,
2165         group_field,
2166         granularity,
2167         template
2168     ) VALUES (
2169         39,
2170         TRUE,
2171         1,
2172         'Print Output for Queued Bib Records',
2173         'vandelay.queued_bib_record.print',
2174         'NOOP_True',
2175         'ProcessTemplate',
2176         'queue.owner',
2177         'print-on-demand',
2178 $$
2179 [%- USE date -%]
2180 <pre>
2181 Queue ID: [% target.0.queue.id %]
2182 Queue Name: [% target.0.queue.name %]
2183 Queue Type: [% target.0.queue.queue_type %]
2184 Complete? [% target.0.queue.complete %]
2185
2186     [% FOR vqbr IN target %]
2187 =-=-=
2188  Title of work    | [% helpers.get_queued_bib_attr('title',vqbr.attributes) %]
2189  Author of work   | [% helpers.get_queued_bib_attr('author',vqbr.attributes) %]
2190  Language of work | [% helpers.get_queued_bib_attr('language',vqbr.attributes) %]
2191  Pagination       | [% helpers.get_queued_bib_attr('pagination',vqbr.attributes) %]
2192  ISBN             | [% helpers.get_queued_bib_attr('isbn',vqbr.attributes) %]
2193  ISSN             | [% helpers.get_queued_bib_attr('issn',vqbr.attributes) %]
2194  Price            | [% helpers.get_queued_bib_attr('price',vqbr.attributes) %]
2195  Accession Number | [% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) %]
2196  TCN Value        | [% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) %]
2197  TCN Source       | [% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) %]
2198  Internal ID      | [% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) %]
2199  Publisher        | [% helpers.get_queued_bib_attr('publisher',vqbr.attributes) %]
2200  Publication Date | [% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) %]
2201  Edition          | [% helpers.get_queued_bib_attr('edition',vqbr.attributes) %]
2202  Item Barcode     | [% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) %]
2203
2204     [% END %]
2205 </pre>
2206 $$
2207     )
2208 ;
2209
2210 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2211     39, 'attributes')
2212     ,( 39, 'queue')
2213 ;
2214
2215 INSERT INTO action_trigger.event_definition (
2216         id,
2217         active,
2218         owner,
2219         name,
2220         hook,
2221         validator,
2222         reactor,
2223         group_field,
2224         granularity,
2225         template
2226     ) VALUES (
2227         40,
2228         TRUE,
2229         1,
2230         'CSV Output for Queued Bib Records',
2231         'vandelay.queued_bib_record.csv',
2232         'NOOP_True',
2233         'ProcessTemplate',
2234         'queue.owner',
2235         'print-on-demand',
2236 $$
2237 [%- USE date -%]
2238 "Title of work","Author of work","Language of work","Pagination","ISBN","ISSN","Price","Accession Number","TCN Value","TCN Source","Internal ID","Publisher","Publication Date","Edition","Item Barcode"
2239 [% FOR vqbr IN target %]"[% helpers.get_queued_bib_attr('title',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('author',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('language',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('pagination',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('isbn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('issn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('price',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('publisher',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('edition',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) | replace('"', '""') %]"
2240 [% END %]
2241 $$
2242     )
2243 ;
2244
2245 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2246     40, 'attributes')
2247     ,( 40, 'queue')
2248 ;
2249
2250 INSERT INTO action_trigger.event_definition (
2251         id,
2252         active,
2253         owner,
2254         name,
2255         hook,
2256         validator,
2257         reactor,
2258         group_field,
2259         granularity,
2260         template
2261     ) VALUES (
2262         41,
2263         TRUE,
2264         1,
2265         'Email Output for Queued Bib Records',
2266         'vandelay.queued_bib_record.email',
2267         'NOOP_True',
2268         'SendEmail',
2269         'queue.owner',
2270         NULL,
2271 $$
2272 [%- USE date -%]
2273 [%- SET user = target.0.queue.owner -%]
2274 To: [%- params.recipient_email || user.email || 'root@localhost' %]
2275 From: [%- params.sender_email || default_sender %]
2276 Subject: Bibs from Import Queue
2277
2278 Queue ID: [% target.0.queue.id %]
2279 Queue Name: [% target.0.queue.name %]
2280 Queue Type: [% target.0.queue.queue_type %]
2281 Complete? [% target.0.queue.complete %]
2282
2283     [% FOR vqbr IN target %]
2284 =-=-=
2285  Title of work    | [% helpers.get_queued_bib_attr('title',vqbr.attributes) %]
2286  Author of work   | [% helpers.get_queued_bib_attr('author',vqbr.attributes) %]
2287  Language of work | [% helpers.get_queued_bib_attr('language',vqbr.attributes) %]
2288  Pagination       | [% helpers.get_queued_bib_attr('pagination',vqbr.attributes) %]
2289  ISBN             | [% helpers.get_queued_bib_attr('isbn',vqbr.attributes) %]
2290  ISSN             | [% helpers.get_queued_bib_attr('issn',vqbr.attributes) %]
2291  Price            | [% helpers.get_queued_bib_attr('price',vqbr.attributes) %]
2292  Accession Number | [% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) %]
2293  TCN Value        | [% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) %]
2294  TCN Source       | [% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) %]
2295  Internal ID      | [% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) %]
2296  Publisher        | [% helpers.get_queued_bib_attr('publisher',vqbr.attributes) %]
2297  Publication Date | [% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) %]
2298  Edition          | [% helpers.get_queued_bib_attr('edition',vqbr.attributes) %]
2299  Item Barcode     | [% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) %]
2300
2301     [% END %]
2302
2303 $$
2304     )
2305 ;
2306
2307 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2308     41, 'attributes')
2309     ,( 41, 'queue')
2310     ,( 41, 'queue.owner')
2311 ;
2312
2313 INSERT INTO action_trigger.event_definition (
2314         id,
2315         active,
2316         owner,
2317         name,
2318         hook,
2319         validator,
2320         reactor,
2321         group_field,
2322         granularity,
2323         template
2324     ) VALUES (
2325         42,
2326         TRUE,
2327         1,
2328         'Print Output for Queued Authority Records',
2329         'vandelay.queued_auth_record.print',
2330         'NOOP_True',
2331         'ProcessTemplate',
2332         'queue.owner',
2333         'print-on-demand',
2334 $$
2335 [%- USE date -%]
2336 <pre>
2337 Queue ID: [% target.0.queue.id %]
2338 Queue Name: [% target.0.queue.name %]
2339 Queue Type: [% target.0.queue.queue_type %]
2340 Complete? [% target.0.queue.complete %]
2341
2342     [% FOR vqar IN target %]
2343 =-=-=
2344  Record Identifier | [% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) %]
2345
2346     [% END %]
2347 </pre>
2348 $$
2349     )
2350 ;
2351
2352 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2353     42, 'attributes')
2354     ,( 42, 'queue')
2355 ;
2356
2357 INSERT INTO action_trigger.event_definition (
2358         id,
2359         active,
2360         owner,
2361         name,
2362         hook,
2363         validator,
2364         reactor,
2365         group_field,
2366         granularity,
2367         template
2368     ) VALUES (
2369         43,
2370         TRUE,
2371         1,
2372         'CSV Output for Queued Authority Records',
2373         'vandelay.queued_auth_record.csv',
2374         'NOOP_True',
2375         'ProcessTemplate',
2376         'queue.owner',
2377         'print-on-demand',
2378 $$
2379 [%- USE date -%]
2380 "Record Identifier"
2381 [% FOR vqar IN target %]"[% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) | replace('"', '""') %]"
2382 [% END %]
2383 $$
2384     )
2385 ;
2386
2387 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2388     43, 'attributes')
2389     ,( 43, 'queue')
2390 ;
2391
2392 INSERT INTO action_trigger.event_definition (
2393         id,
2394         active,
2395         owner,
2396         name,
2397         hook,
2398         validator,
2399         reactor,
2400         group_field,
2401         granularity,
2402         template
2403     ) VALUES (
2404         44,
2405         TRUE,
2406         1,
2407         'Email Output for Queued Authority Records',
2408         'vandelay.queued_auth_record.email',
2409         'NOOP_True',
2410         'SendEmail',
2411         'queue.owner',
2412         NULL,
2413 $$
2414 [%- USE date -%]
2415 [%- SET user = target.0.queue.owner -%]
2416 To: [%- params.recipient_email || user.email || 'root@localhost' %]
2417 From: [%- params.sender_email || default_sender %]
2418 Subject: Authorities from Import Queue
2419
2420 Queue ID: [% target.0.queue.id %]
2421 Queue Name: [% target.0.queue.name %]
2422 Queue Type: [% target.0.queue.queue_type %]
2423 Complete? [% target.0.queue.complete %]
2424
2425     [% FOR vqar IN target %]
2426 =-=-=
2427  Record Identifier | [% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) %]
2428
2429     [% END %]
2430
2431 $$
2432     )
2433 ;
2434
2435 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2436     44, 'attributes')
2437     ,( 44, 'queue')
2438     ,( 44, 'queue.owner')
2439 ;
2440
2441 INSERT INTO action_trigger.event_definition (
2442         id,
2443         active,
2444         owner,
2445         name,
2446         hook,
2447         validator,
2448         reactor,
2449         group_field,
2450         granularity,
2451         template
2452     ) VALUES (
2453         45,
2454         TRUE,
2455         1,
2456         'Print Output for Import Items from Queued Bib Records',
2457         'vandelay.import_items.print',
2458         'NOOP_True',
2459         'ProcessTemplate',
2460         'record.queue.owner',
2461         'print-on-demand',
2462 $$
2463 [%- USE date -%]
2464 <pre>
2465 Queue ID: [% target.0.record.queue.id %]
2466 Queue Name: [% target.0.record.queue.name %]
2467 Queue Type: [% target.0.record.queue.queue_type %]
2468 Complete? [% target.0.record.queue.complete %]
2469
2470     [% FOR vii IN target %]
2471 =-=-=
2472  Import Item ID         | [% vii.id %]
2473  Title of work          | [% helpers.get_queued_bib_attr('title',vii.record.attributes) %]
2474  ISBN                   | [% helpers.get_queued_bib_attr('isbn',vii.record.attributes) %]
2475  Attribute Definition   | [% vii.definition %]
2476  Import Error           | [% vii.import_error %]
2477  Import Error Detail    | [% vii.error_detail %]
2478  Owning Library         | [% vii.owning_lib %]
2479  Circulating Library    | [% vii.circ_lib %]
2480  Call Number            | [% vii.call_number %]
2481  Copy Number            | [% vii.copy_number %]
2482  Status                 | [% vii.status.name %]
2483  Shelving Location      | [% vii.location.name %]
2484  Circulate              | [% vii.circulate %]
2485  Deposit                | [% vii.deposit %]
2486  Deposit Amount         | [% vii.deposit_amount %]
2487  Reference              | [% vii.ref %]
2488  Holdable               | [% vii.holdable %]
2489  Price                  | [% vii.price %]
2490  Barcode                | [% vii.barcode %]
2491  Circulation Modifier   | [% vii.circ_modifier %]
2492  Circulate As MARC Type | [% vii.circ_as_type %]
2493  Alert Message          | [% vii.alert_message %]
2494  Public Note            | [% vii.pub_note %]
2495  Private Note           | [% vii.priv_note %]
2496  OPAC Visible           | [% vii.opac_visible %]
2497
2498     [% END %]
2499 </pre>
2500 $$
2501     )
2502 ;
2503
2504 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2505     45, 'record')
2506     ,( 45, 'record.attributes')
2507     ,( 45, 'record.queue')
2508     ,( 45, 'record.queue.owner')
2509 ;
2510
2511 INSERT INTO action_trigger.event_definition (
2512         id,
2513         active,
2514         owner,
2515         name,
2516         hook,
2517         validator,
2518         reactor,
2519         group_field,
2520         granularity,
2521         template
2522     ) VALUES (
2523         46,
2524         TRUE,
2525         1,
2526         'CSV Output for Import Items from Queued Bib Records',
2527         'vandelay.import_items.csv',
2528         'NOOP_True',
2529         'ProcessTemplate',
2530         'record.queue.owner',
2531         'print-on-demand',
2532 $$
2533 [%- USE date -%]
2534 "Import Item ID","Title of work","ISBN","Attribute Definition","Import Error","Import Error Detail","Owning Library","Circulating Library","Call Number","Copy Number","Status","Shelving Location","Circulate","Deposit","Deposit Amount","Reference","Holdable","Price","Barcode","Circulation Modifier","Circulate As MARC Type","Alert Message","Public Note","Private Note","OPAC Visible"
2535 [% FOR vii IN target %]"[% vii.id | replace('"', '""') %]","[% helpers.get_queued_bib_attr('title',vii.record.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('isbn',vii.record.attributes) | replace('"', '""') %]","[% vii.definition | replace('"', '""') %]","[% vii.import_error | replace('"', '""') %]","[% vii.error_detail | replace('"', '""') %]","[% vii.owning_lib | replace('"', '""') %]","[% vii.circ_lib | replace('"', '""') %]","[% vii.call_number | replace('"', '""') %]","[% vii.copy_number | replace('"', '""') %]","[% vii.status.name | replace('"', '""') %]","[% vii.location.name | replace('"', '""') %]","[% vii.circulate | replace('"', '""') %]","[% vii.deposit | replace('"', '""') %]","[% vii.deposit_amount | replace('"', '""') %]","[% vii.ref | replace('"', '""') %]","[% vii.holdable | replace('"', '""') %]","[% vii.price | replace('"', '""') %]","[% vii.barcode | replace('"', '""') %]","[% vii.circ_modifier | replace('"', '""') %]","[% vii.circ_as_type | replace('"', '""') %]","[% vii.alert_message | replace('"', '""') %]","[% vii.pub_note | replace('"', '""') %]","[% vii.priv_note | replace('"', '""') %]","[% vii.opac_visible | replace('"', '""') %]"
2536 [% END %]
2537 $$
2538     )
2539 ;
2540
2541 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2542     46, 'record')
2543     ,( 46, 'record.attributes')
2544     ,( 46, 'record.queue')
2545     ,( 46, 'record.queue.owner')
2546 ;
2547
2548 INSERT INTO action_trigger.event_definition (
2549         id,
2550         active,
2551         owner,
2552         name,
2553         hook,
2554         validator,
2555         reactor,
2556         group_field,
2557         granularity,
2558         template
2559     ) VALUES (
2560         47,
2561         TRUE,
2562         1,
2563         'Email Output for Import Items from Queued Bib Records',
2564         'vandelay.import_items.email',
2565         'NOOP_True',
2566         'SendEmail',
2567         'record.queue.owner',
2568         NULL,
2569 $$
2570 [%- USE date -%]
2571 [%- SET user = target.0.record.queue.owner -%]
2572 To: [%- params.recipient_email || user.email || 'root@localhost' %]
2573 From: [%- params.sender_email || default_sender %]
2574 Subject: Import Items from Import Queue
2575
2576 Queue ID: [% target.0.record.queue.id %]
2577 Queue Name: [% target.0.record.queue.name %]
2578 Queue Type: [% target.0.record.queue.queue_type %]
2579 Complete? [% target.0.record.queue.complete %]
2580
2581     [% FOR vii IN target %]
2582 =-=-=
2583  Import Item ID         | [% vii.id %]
2584  Title of work          | [% helpers.get_queued_bib_attr('title',vii.record.attributes) %]
2585  ISBN                   | [% helpers.get_queued_bib_attr('isbn',vii.record.attributes) %]
2586  Attribute Definition   | [% vii.definition %]
2587  Import Error           | [% vii.import_error %]
2588  Import Error Detail    | [% vii.error_detail %]
2589  Owning Library         | [% vii.owning_lib %]
2590  Circulating Library    | [% vii.circ_lib %]
2591  Call Number            | [% vii.call_number %]
2592  Copy Number            | [% vii.copy_number %]
2593  Status                 | [% vii.status.name %]
2594  Shelving Location      | [% vii.location.name %]
2595  Circulate              | [% vii.circulate %]
2596  Deposit                | [% vii.deposit %]
2597  Deposit Amount         | [% vii.deposit_amount %]
2598  Reference              | [% vii.ref %]
2599  Holdable               | [% vii.holdable %]
2600  Price                  | [% vii.price %]
2601  Barcode                | [% vii.barcode %]
2602  Circulation Modifier   | [% vii.circ_modifier %]
2603  Circulate As MARC Type | [% vii.circ_as_type %]
2604  Alert Message          | [% vii.alert_message %]
2605  Public Note            | [% vii.pub_note %]
2606  Private Note           | [% vii.priv_note %]
2607  OPAC Visible           | [% vii.opac_visible %]
2608
2609     [% END %]
2610 $$
2611     )
2612 ;
2613
2614 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2615     47, 'record')
2616     ,( 47, 'record.attributes')
2617     ,( 47, 'record.queue')
2618     ,( 47, 'record.queue.owner')
2619 ;
2620
2621
2622
2623 SELECT evergreen.upgrade_deps_block_check('0574', :eg_version);
2624
2625 UPDATE action_trigger.event_definition SET template =
2626 $$
2627 [%- USE date -%]
2628 <style>
2629     table { border-collapse: collapse; }
2630     td { padding: 5px; border-bottom: 1px solid #888; }
2631     th { font-weight: bold; }
2632 </style>
2633 [%
2634     # Sort the holds into copy-location buckets
2635     # In the main print loop, sort each bucket by callnumber before printing
2636     SET holds_list = [];
2637     SET loc_data = [];
2638     SET current_location = target.0.current_copy.location.id;
2639     FOR hold IN target;
2640         IF current_location != hold.current_copy.location.id;
2641             SET current_location = hold.current_copy.location.id;
2642             holds_list.push(loc_data);
2643             SET loc_data = [];
2644         END;
2645         SET hold_data = {
2646             'hold' => hold,
2647             'callnumber' => hold.current_copy.call_number.label
2648         };
2649         loc_data.push(hold_data);
2650     END;
2651     holds_list.push(loc_data)
2652 %]
2653 <table>
2654     <thead>
2655         <tr>
2656             <th>Title</th>
2657             <th>Author</th>
2658             <th>Shelving Location</th>
2659             <th>Call Number</th>
2660             <th>Barcode/Part</th>
2661             <th>Patron</th>
2662         </tr>
2663     </thead>
2664     <tbody>
2665     [% FOR loc_data IN holds_list  %]
2666         [% FOR hold_data IN loc_data.sort('callnumber') %]
2667             [%
2668                 SET hold = hold_data.hold;
2669                 SET copy_data = helpers.get_copy_bib_basics(hold.current_copy.id);
2670             %]
2671             <tr>
2672                 <td>[% copy_data.title | truncate %]</td>
2673                 <td>[% copy_data.author | truncate %]</td>
2674                 <td>[% hold.current_copy.location.name %]</td>
2675                 <td>[% hold.current_copy.call_number.label %]</td>
2676                 <td>[% hold.current_copy.barcode %]
2677                     [% FOR part IN hold.current_copy.parts %]
2678                        [% part.part.label %]
2679                     [% END %]
2680                 </td>
2681                 <td>[% hold.usr.card.barcode %]</td>
2682             </tr>
2683         [% END %]
2684     [% END %]
2685     <tbody>
2686 </table>
2687 $$
2688     WHERE id = 35;
2689
2690 INSERT INTO action_trigger.environment (
2691         event_def,
2692         path
2693     ) VALUES
2694         (35, 'current_copy.parts'),
2695         (35, 'current_copy.parts.part')
2696 ;
2697
2698
2699 -- Evergreen DB patch XXXX.schema.authority-control-sets.sql
2700 --
2701 -- Schema upgrade to add Authority Control Set functionality
2702 --
2703
2704
2705 -- check whether patch can be applied
2706 SELECT evergreen.upgrade_deps_block_check('0575', :eg_version);
2707
2708 CREATE TABLE authority.control_set (
2709     id          SERIAL  PRIMARY KEY,
2710     name        TEXT    NOT NULL UNIQUE, -- i18n
2711     description TEXT                     -- i18n
2712 );
2713
2714 CREATE TABLE authority.control_set_authority_field (
2715     id          SERIAL  PRIMARY KEY,
2716     main_entry  INT     REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2717     control_set INT     NOT NULL REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2718     tag         CHAR(3) NOT NULL,
2719     nfi CHAR(1),
2720     sf_list     TEXT    NOT NULL,
2721     name        TEXT    NOT NULL, -- i18n
2722     description TEXT              -- i18n
2723 );
2724
2725 CREATE TABLE authority.control_set_bib_field (
2726     id              SERIAL  PRIMARY KEY,
2727     authority_field INT     NOT NULL REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2728     tag             CHAR(3) NOT NULL
2729 );
2730
2731 CREATE TABLE authority.thesaurus (
2732     code        TEXT    PRIMARY KEY,     -- MARC21 thesaurus code
2733     control_set INT     NOT NULL REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2734     name        TEXT    NOT NULL UNIQUE, -- i18n
2735     description TEXT                     -- i18n
2736 );
2737
2738 CREATE TABLE authority.browse_axis (
2739     code        TEXT    PRIMARY KEY,
2740     name        TEXT    UNIQUE NOT NULL, -- i18n
2741     sorter      TEXT    REFERENCES config.record_attr_definition (name) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2742     description TEXT
2743 );
2744
2745 CREATE TABLE authority.browse_axis_authority_field_map (
2746     id          SERIAL  PRIMARY KEY,
2747     axis        TEXT    NOT NULL REFERENCES authority.browse_axis (code) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2748     field       INT     NOT NULL REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
2749 );
2750
2751 ALTER TABLE authority.record_entry ADD COLUMN control_set INT REFERENCES authority.control_set (id) ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED;
2752 ALTER TABLE authority.rec_descriptor DROP COLUMN char_encoding, ADD COLUMN encoding_level TEXT, ADD COLUMN thesaurus TEXT;
2753
2754 CREATE INDEX authority_full_rec_value_index ON authority.full_rec (value);
2755 CREATE OR REPLACE RULE protect_authority_rec_delete AS ON DELETE TO authority.record_entry DO INSTEAD (UPDATE authority.record_entry SET deleted = TRUE WHERE OLD.id = authority.record_entry.id; DELETE FROM authority.full_rec WHERE record = OLD.id);
2756  
2757 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT, no_thesaurus BOOL ) RETURNS TEXT AS $func$
2758 DECLARE
2759     acsaf           authority.control_set_authority_field%ROWTYPE;
2760     tag_used        TEXT;
2761     sf              TEXT;
2762     thes_code       TEXT;
2763     cset            INT;
2764     heading_text    TEXT;
2765     tmp_text        TEXT;
2766 BEGIN
2767     thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
2768     IF thes_code IS NULL THEN
2769         thes_code := '|';
2770     END IF;
2771
2772     SELECT control_set INTO cset FROM authority.thesaurus WHERE code = thes_code;
2773     IF NOT FOUND THEN
2774         cset = 1;
2775     END IF;
2776
2777     heading_text := '';
2778     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset AND main_entry IS NULL LOOP
2779         tag_used := acsaf.tag;
2780         FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
2781             tmp_text := oils_xpath_string('//*[@tag="'||tag_used||'"]/*[@code="'||sf||'"]', marcxml);
2782             IF tmp_text IS NOT NULL AND tmp_text <> '' THEN
2783                 heading_text := heading_text || E'\u2021' || sf || ' ' || tmp_text;
2784             END IF;
2785         END LOOP;
2786         EXIT WHEN heading_text <> '';
2787     END LOOP;
2788  
2789     IF thes_code = 'z' THEN
2790         thes_code := oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml);
2791     END IF;
2792
2793     IF heading_text <> '' THEN
2794         IF no_thesaurus IS TRUE THEN
2795             heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
2796         ELSE
2797             heading_text := tag_used || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
2798         END IF;
2799     ELSE
2800         heading_text := 'NOHEADING_' || thes_code || ' ' || MD5(marcxml);
2801     END IF;
2802
2803     RETURN heading_text;
2804 END;
2805 $func$ LANGUAGE PLPGSQL IMMUTABLE;
2806
2807 CREATE OR REPLACE FUNCTION authority.simple_normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
2808     SELECT authority.normalize_heading($1, TRUE);
2809 $func$ LANGUAGE SQL IMMUTABLE;
2810
2811 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
2812     SELECT authority.normalize_heading($1, FALSE);
2813 $func$ LANGUAGE SQL IMMUTABLE;
2814
2815 CREATE OR REPLACE VIEW authority.tracing_links AS
2816     SELECT  main.record AS record,
2817             main.id AS main_id,
2818             main.tag AS main_tag,
2819             oils_xpath_string('//*[@tag="'||main.tag||'"]/*[local-name()="subfield"]', are.marc) AS main_value,
2820             substr(link.value,1,1) AS relationship,
2821             substr(link.value,2,1) AS use_restriction,
2822             substr(link.value,3,1) AS deprecation,
2823             substr(link.value,4,1) AS display_restriction,
2824             link.id AS link_id,
2825             link.tag AS link_tag,
2826             oils_xpath_string('//*[@tag="'||link.tag||'"]/*[local-name()="subfield"]', are.marc) AS link_value,
2827             authority.normalize_heading(are.marc) AS normalized_main_value
2828       FROM  authority.full_rec main
2829             JOIN authority.record_entry are ON (main.record = are.id)
2830             JOIN authority.control_set_authority_field main_entry
2831                 ON (main_entry.tag = main.tag
2832                     AND main_entry.main_entry IS NULL
2833                     AND main.subfield = 'a' )
2834             JOIN authority.control_set_authority_field sub_entry
2835                 ON (main_entry.id = sub_entry.main_entry)
2836             JOIN authority.full_rec link
2837                 ON (link.record = main.record
2838                     AND link.tag = sub_entry.tag
2839                     AND link.subfield = 'w' );
2840  
2841 CREATE OR REPLACE FUNCTION authority.generate_overlay_template (source_xml TEXT) RETURNS TEXT AS $f$
2842 DECLARE
2843     cset                INT;
2844     main_entry          authority.control_set_authority_field%ROWTYPE;
2845     bib_field           authority.control_set_bib_field%ROWTYPE;
2846     auth_id             INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', source_xml)::INT;
2847     replace_data        XML[] DEFAULT '{}'::XML[];
2848     replace_rules       TEXT[] DEFAULT '{}'::TEXT[];
2849     auth_field          XML[];
2850 BEGIN
2851     IF auth_id IS NULL THEN
2852         RETURN NULL;
2853     END IF;
2854
2855     -- Default to the LoC controll set
2856     SELECT COALESCE(control_set,1) INTO cset FROM authority.record_entry WHERE id = auth_id;
2857
2858     FOR main_entry IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
2859         auth_field := XPATH('//*[@tag="'||main_entry.tag||'"][1]',source_xml::XML);
2860         IF ARRAY_LENGTH(auth_field,1) > 0 THEN
2861             FOR bib_field IN SELECT * FROM authority.control_set_bib_field WHERE authority_field = main_entry.id LOOP
2862                 replace_data := replace_data || XMLELEMENT( name datafield, XMLATTRIBUTES(bib_field.tag AS tag), XPATH('//*[local-name()="subfield"]',auth_field[1])::XML[]);
2863                 replace_rules := replace_rules || ( bib_field.tag || main_entry.sf_list || E'[0~\\)' || auth_id || '$]' );
2864             END LOOP;
2865             EXIT;
2866         END IF;
2867     END LOOP;
2868  
2869     RETURN XMLELEMENT(
2870         name record,
2871         XMLATTRIBUTES('http://www.loc.gov/MARC21/slim' AS xmlns),
2872         XMLELEMENT( name leader, '00881nam a2200193   4500'),
2873         replace_data,
2874         XMLELEMENT(
2875             name datafield,
2876             XMLATTRIBUTES( '905' AS tag, ' ' AS ind1, ' ' AS ind2),
2877             XMLELEMENT(
2878                 name subfield,
2879                 XMLATTRIBUTES('r' AS code),
2880                 ARRAY_TO_STRING(replace_rules,',')
2881             )
2882         )
2883     )::TEXT;
2884 END;
2885 $f$ STABLE LANGUAGE PLPGSQL;
2886  
2887 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( BIGINT ) RETURNS TEXT AS $func$
2888     SELECT authority.generate_overlay_template( marc ) FROM authority.record_entry WHERE id = $1;
2889 $func$ LANGUAGE SQL;
2890  
2891 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
2892
2893     use MARC::Record;
2894     use MARC::File::XML (BinaryEncoding => 'UTF-8');
2895     use MARC::Charset;
2896     use strict;
2897
2898     MARC::Charset->assume_unicode(1);
2899
2900     my $target_xml = shift;
2901     my $source_xml = shift;
2902     my $field_spec = shift;
2903     my $force_add = shift || 0;
2904
2905     my $target_r = MARC::Record->new_from_xml( $target_xml );
2906     my $source_r = MARC::Record->new_from_xml( $source_xml );
2907
2908     return $target_xml unless ($target_r && $source_r);
2909
2910     my @field_list = split(',', $field_spec);
2911
2912     my %fields;
2913     for my $f (@field_list) {
2914         $f =~ s/^\s*//; $f =~ s/\s*$//;
2915         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
2916             my $field = $1;
2917             $field =~ s/\s+//;
2918             my $sf = $2;
2919             $sf =~ s/\s+//;
2920             my $match = $3;
2921             $match =~ s/^\s*//; $match =~ s/\s*$//;
2922             $fields{$field} = { sf => [ split('', $sf) ] };
2923             if ($match) {
2924                 my ($msf,$mre) = split('~', $match);
2925                 if (length($msf) > 0 and length($mre) > 0) {
2926                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
2927                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
2928                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
2929                 }
2930             }
2931         }
2932     }
2933
2934     for my $f ( keys %fields) {
2935         if ( @{$fields{$f}{sf}} ) {
2936             for my $from_field ($source_r->field( $f )) {
2937                 my @tos = $target_r->field( $f );
2938                 if (!@tos) {
2939                     next if (exists($fields{$f}{match}) and !$force_add);
2940                     my @new_fields = map { $_->clone } $source_r->field( $f );
2941                     $target_r->insert_fields_ordered( @new_fields );
2942                 } else {
2943                     for my $to_field (@tos) {
2944                         if (exists($fields{$f}{match})) {
2945                             next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
2946                         }
2947                         my @new_sf = map { ($_ => $from_field->subfield($_)) } grep { defined($from_field->subfield($_)) } @{$fields{$f}{sf}};
2948                         $to_field->add_subfields( @new_sf );
2949                     }
2950                 }
2951             }
2952         } else {
2953             my @new_fields = map { $_->clone } $source_r->field( $f );
2954             $target_r->insert_fields_ordered( @new_fields );
2955         }
2956     }
2957
2958     $target_xml = $target_r->as_xml_record;
2959     $target_xml =~ s/^<\?.+?\?>$//mo;
2960     $target_xml =~ s/\n//sgo;
2961     $target_xml =~ s/>\s+</></sgo;
2962
2963     return $target_xml;
2964
2965 $_$ LANGUAGE PLPERLU;
2966
2967
2968 CREATE INDEX by_heading ON authority.record_entry (authority.simple_normalize_heading(marc)) WHERE deleted IS FALSE or deleted = FALSE;
2969
2970 INSERT INTO config.metabib_field ( id, field_class, name, label, format, xpath, search_field, facet_field) VALUES
2971     (28, 'identifier', 'authority_id', oils_i18n_gettext(28, 'Authority Record ID', 'cmf', 'label'), 'marcxml', '//marc:datafield/marc:subfield[@code="0"]', FALSE, TRUE);
2972  
2973 INSERT INTO config.marc21_rec_type_map (code, type_val, blvl_val) VALUES ('AUT','z',' ');
2974 INSERT INTO config.marc21_rec_type_map (code, type_val, blvl_val) VALUES ('MFHD','uvxy',' ');
2975  
2976 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('ELvl', 'ldr', 'AUT', 17, 1, ' ');
2977 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Subj', '008', 'AUT', 11, 1, '|');
2978 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('RecStat', 'ldr', 'AUT', 5, 1, 'n');
2979  
2980 INSERT INTO config.metabib_field_index_norm_map (field,norm,pos)
2981     SELECT  m.id,
2982             i.id,
2983             -1
2984       FROM  config.metabib_field m,
2985             config.index_normalizer i
2986       WHERE i.func = 'remove_paren_substring'
2987             AND m.id IN (28);
2988
2989 SELECT SETVAL('authority.control_set_id_seq'::TEXT, 100);
2990 SELECT SETVAL('authority.control_set_authority_field_id_seq'::TEXT, 1000);
2991 SELECT SETVAL('authority.control_set_bib_field_id_seq'::TEXT, 1000);
2992
2993 INSERT INTO authority.control_set (id, name, description) VALUES (
2994     1,
2995     oils_i18n_gettext('1','LoC','acs','name'),
2996     oils_i18n_gettext('1','Library of Congress standard authority record control semantics','acs','description')
2997 );
2998
2999 INSERT INTO authority.control_set_authority_field (id, control_set, main_entry, tag, sf_list, name) VALUES
3000
3001 -- Main entries
3002     (1, 1, NULL, '100', 'abcdefklmnopqrstvxyz', oils_i18n_gettext('1','Heading -- Personal Name','acsaf','name')),
3003     (2, 1, NULL, '110', 'abcdefgklmnoprstvxyz', oils_i18n_gettext('2','Heading -- Corporate Name','acsaf','name')),
3004     (3, 1, NULL, '111', 'acdefgklnpqstvxyz', oils_i18n_gettext('3','Heading -- Meeting Name','acsaf','name')),
3005     (4, 1, NULL, '130', 'adfgklmnoprstvxyz', oils_i18n_gettext('4','Heading -- Uniform Title','acsaf','name')),
3006     (5, 1, NULL, '150', 'abvxyz', oils_i18n_gettext('5','Heading -- Topical Term','acsaf','name')),
3007     (6, 1, NULL, '151', 'avxyz', oils_i18n_gettext('6','Heading -- Geographic Name','acsaf','name')),
3008     (7, 1, NULL, '155', 'avxyz', oils_i18n_gettext('7','Heading -- Genre/Form Term','acsaf','name')),
3009     (8, 1, NULL, '180', 'vxyz', oils_i18n_gettext('8','Heading -- General Subdivision','acsaf','name')),
3010     (9, 1, NULL, '181', 'vxyz', oils_i18n_gettext('9','Heading -- Geographic Subdivision','acsaf','name')),
3011     (10, 1, NULL, '182', 'vxyz', oils_i18n_gettext('10','Heading -- Chronological Subdivision','acsaf','name')),
3012     (11, 1, NULL, '185', 'vxyz', oils_i18n_gettext('11','Heading -- Form Subdivision','acsaf','name')),
3013     (12, 1, NULL, '148', 'avxyz', oils_i18n_gettext('12','Heading -- Chronological Term','acsaf','name')),
3014
3015 -- See Also From tracings
3016     (21, 1, 1, '500', 'abcdefiklmnopqrstvwxyz4', oils_i18n_gettext('21','See Also From Tracing -- Personal Name','acsaf','name')),
3017     (22, 1, 2, '510', 'abcdefgiklmnoprstvwxyz4', oils_i18n_gettext('22','See Also From Tracing -- Corporate Name','acsaf','name')),
3018     (23, 1, 3, '511', 'acdefgiklnpqstvwxyz4', oils_i18n_gettext('23','See Also From Tracing -- Meeting Name','acsaf','name')),
3019     (24, 1, 4, '530', 'adfgiklmnoprstvwxyz4', oils_i18n_gettext('24','See Also From Tracing -- Uniform Title','acsaf','name')),
3020     (25, 1, 5, '550', 'abivwxyz4', oils_i18n_gettext('25','See Also From Tracing -- Topical Term','acsaf','name')),
3021     (26, 1, 6, '551', 'aivwxyz4', oils_i18n_gettext('26','See Also From Tracing -- Geographic Name','acsaf','name')),
3022     (27, 1, 7, '555', 'aivwxyz4', oils_i18n_gettext('27','See Also From Tracing -- Genre/Form Term','acsaf','name')),
3023     (28, 1, 8, '580', 'ivwxyz4', oils_i18n_gettext('28','See Also From Tracing -- General Subdivision','acsaf','name')),
3024     (29, 1, 9, '581', 'ivwxyz4', oils_i18n_gettext('29','See Also From Tracing -- Geographic Subdivision','acsaf','name')),
3025     (30, 1, 10, '582', 'ivwxyz4', oils_i18n_gettext('30','See Also From Tracing -- Chronol