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