1 --Upgrade Script for 2.1 to 2.2-alpha2
2 -- DROP objects that might have existed from a prior run of 0526
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);
10 INSERT INTO config.upgrade_log (version) VALUES ('2.2-alpha3');
12 INSERT INTO config.upgrade_log (version) VALUES ('0526'); --miker
14 CREATE TABLE config.db_patch_dependencies (
15 db_patch TEXT PRIMARY KEY,
20 CREATE OR REPLACE FUNCTION evergreen.array_overlap_check (/* field */) RETURNS TRIGGER AS $$
26 EXECUTE 'SELECT COUNT(*) FROM '|| TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME ||' WHERE '|| fld ||' && ($1).'|| fld INTO cnt USING NEW;
28 RAISE EXCEPTION 'Cannot insert duplicate array into field % of table %', fld, TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME;
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');
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');
42 ALTER TABLE config.upgrade_log
43 ADD COLUMN applied_to TEXT;
45 -- Provide a named type for patching functions
46 CREATE TYPE evergreen.patch AS (patch TEXT);
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)
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)
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 $$
67 FROM config.db_patch_dependencies
68 WHERE ARRAY[$1]::TEXT[] && deprecates
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 $$
74 FROM config.db_patch_dependencies
75 WHERE ARRAY[$1]::TEXT[] && supersedes
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 $$
81 FROM (SELECT * FROM evergreen.upgrade_list_applied_deprecates( $1 )
83 SELECT * FROM evergreen.upgrade_list_applied_supersedes( $1 )
85 SELECT * FROM evergreen.upgrade_list_applied_deprecated( $1 )
87 SELECT * FROM evergreen.upgrade_list_applied_superseded( $1 ))x
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 $$
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);
100 Upgrade script % can not be applied:
101 applied deprecated scripts %
102 applied superseded scripts %
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);
112 INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
117 -- Evergreen DB patch 0536.schema.lazy_circ-barcode_lookup.sql
119 -- FIXME: insert description of change, if needed
122 -- check whether patch can be applied
123 INSERT INTO config.upgrade_log (version) VALUES ('0536');
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');
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,
133 length INT NOT NULL DEFAULT 0,
135 padding_end BOOL NOT NULL DEFAULT false,
136 asset BOOL NOT NULL DEFAULT true,
137 actor BOOL NOT NULL DEFAULT true
140 CREATE TYPE evergreen.barcode_set AS (type TEXT, id BIGINT, barcode TEXT);
142 CREATE OR REPLACE FUNCTION evergreen.get_barcodes(select_ou INT, type TEXT, in_barcode TEXT) RETURNS SETOF evergreen.barcode_set AS $$
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;
156 IF position('asset' in type) > 0 THEN
159 IF position('serial' in type) > 0 THEN
162 IF position('booking' in type) > 0 THEN
165 IF do_asset OR do_serial OR do_booking THEN
166 asset_barcodes = asset_barcodes || in_barcode;
168 IF position('actor' in type) > 0 THEN
170 actor_barcodes = actor_barcodes || in_barcode;
173 barcode_len := length(in_barcode);
175 FOR completion_set IN
176 SELECT * FROM config.barcode_completion
178 AND org_unit IN (SELECT aou.id FROM actor.org_unit_ancestors(select_ou) aou)
180 IF completion_set.prefix IS NULL THEN
181 completion_set.prefix := '';
183 IF completion_set.suffix IS NULL THEN
184 completion_set.suffix := '';
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;
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);
194 cur_barcode = lpad(in_barcode, completion_len, completion_set.padding);
196 cur_barcode = completion_set.prefix || cur_barcode || completion_set.suffix;
199 IF completion_set.actor THEN
200 actor_barcodes = actor_barcodes || cur_barcode;
202 IF completion_set.asset THEN
203 asset_barcodes = asset_barcodes || cur_barcode;
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;
211 RETURN QUERY SELECT 'asset'::TEXT, id, barcode FROM asset.copy WHERE barcode = ANY(asset_barcodes) AND deleted = false;
213 RETURN QUERY SELECT 'serial'::TEXT, id, barcode FROM serial.unit WHERE barcode = ANY(asset_barcodes) AND deleted = false;
216 RETURN QUERY SELECT 'booking'::TEXT, id::BIGINT, barcode FROM booking.resource WHERE barcode = ANY(asset_barcodes);
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;
225 COMMENT ON FUNCTION evergreen.get_barcodes(INT, TEXT, TEXT) IS $$
226 Given user input, find an appropriate barcode in the proper class.
228 Will add prefix/suffix information to do so, and return all results.
233 INSERT INTO config.upgrade_log (version) VALUES ('0537'); --miker
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);
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 $$
243 FROM config.db_patch_dependencies
244 WHERE ARRAY[$1]::TEXT[] && deprecates
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 $$
250 FROM config.db_patch_dependencies
251 WHERE ARRAY[$1]::TEXT[] && supersedes
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 $$
257 FROM (SELECT * FROM evergreen.upgrade_list_applied_deprecates( $1 )
259 SELECT * FROM evergreen.upgrade_list_applied_supersedes( $1 )
261 SELECT * FROM evergreen.upgrade_list_applied_deprecated( $1 )
263 SELECT * FROM evergreen.upgrade_list_applied_superseded( $1 ))x
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 $$
269 IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
271 Upgrade script % can not be applied:
272 applied deprecated scripts %
273 applied superseded scripts %
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);
283 INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
289 INSERT INTO config.upgrade_log (version) VALUES ('0544');
291 INSERT INTO config.usr_setting_type
292 ( name, opac_visible, label, description, datatype) VALUES
293 ( 'circ.collections.exempt',
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'),
302 SELECT evergreen.upgrade_deps_block_check('0545', :eg_version);
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'));
308 --- stock Circulation Administrator group
310 INSERT INTO permission.grp_perm_map ( grp, perm, depth, grantable )
316 FROM permission.perm_list
317 WHERE code in ('ABORT_TRANSIT_ON_LOST', 'ABORT_TRANSIT_ON_MISSING');
319 -- Evergreen DB patch 0546.schema.sip_statcats.sql
322 -- check whether patch can be applied
323 SELECT evergreen.upgrade_deps_block_check('0546', :eg_version);
325 CREATE TABLE actor.stat_cat_sip_fields (
326 field CHAR(2) PRIMARY KEY,
328 one_only BOOL NOT NULL DEFAULT FALSE
330 COMMENT ON TABLE actor.stat_cat_sip_fields IS $$
331 Actor Statistical Category SIP Fields
333 Contains the list of valid SIP Field identifiers for
334 Statistical Categories.
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;
340 CREATE FUNCTION actor.stat_cat_check() RETURNS trigger AS $func$
342 sipfield actor.stat_cat_sip_fields%ROWTYPE;
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';
356 $func$ LANGUAGE PLPGSQL;
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();
362 CREATE TABLE asset.stat_cat_sip_fields (
363 field CHAR(2) PRIMARY KEY,
365 one_only BOOL NOT NULL DEFAULT FALSE
367 COMMENT ON TABLE asset.stat_cat_sip_fields IS $$
368 Asset Statistical Category SIP Fields
370 Contains the list of valid SIP Field identifiers for
371 Statistical Categories.
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;
378 CREATE FUNCTION asset.stat_cat_check() RETURNS trigger AS $func$
380 sipfield asset.stat_cat_sip_fields%ROWTYPE;
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';
394 $func$ LANGUAGE PLPGSQL;
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();
402 SELECT evergreen.upgrade_deps_block_check('0548', :eg_version); -- dbwells
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
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')
413 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
415 pgt.id, perm.id, aout.depth, TRUE
417 permission.grp_tree pgt,
418 permission.perm_list perm,
419 actor.org_unit_type aout
421 pgt.name = 'Circulation Administrator' AND
422 aout.name = 'Consortium' AND
424 'ABORT_TRANSIT_ON_LOST',
425 'ABORT_TRANSIT_ON_MISSING'
428 FROM permission.grp_perm_map AS map
431 AND map.perm = perm.id
434 -- Evergreen DB patch XXXX.data.transit-checkin-interval.sql
436 -- New org unit setting "circ.transit.min_checkin_interval"
437 -- New TRANSIT_CHECKIN_INTERVAL_BLOCK.override permission
441 -- check whether patch can be applied
442 SELECT evergreen.upgrade_deps_block_check('0549', :eg_version);
444 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
445 'circ.transit.min_checkin_interval',
447 'circ.transit.min_checkin_interval',
448 'Circ: Minimum Transit Checkin Interval',
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',
461 INSERT INTO permission.perm_list ( id, code, description ) VALUES (
463 'TRANSIT_CHECKIN_INTERVAL_BLOCK.override',
466 'Allows a user to override the TRANSIT_CHECKIN_INTERVAL_BLOCK event',
472 -- add the perm to the default circ admin group
473 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
475 pgt.id, perm.id, aout.depth, TRUE
477 permission.grp_tree pgt,
478 permission.perm_list perm,
479 actor.org_unit_type aout
481 pgt.name = 'Circulation Administrator' AND
482 aout.name = 'System' AND
483 perm.code IN ( 'TRANSIT_CHECKIN_INTERVAL_BLOCK.override' );
486 -- check whether patch can be applied
487 SELECT evergreen.upgrade_deps_block_check('0550', :eg_version);
489 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
490 'org.patron_opt_boundary',
492 'org.patron_opt_boundary',
493 'Circ: Patron Opt-In Boundary',
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.',
506 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
507 'org.patron_opt_default',
509 'org.patron_opt_default',
510 'Circ: Patron Opt-In Default',
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.',
523 -- Evergreen DB patch 0562.schema.copy_active_date.sql
528 -- check whether patch can be applied
529 SELECT evergreen.upgrade_deps_block_check('0562', :eg_version);
531 ALTER TABLE asset.copy
532 ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
534 ALTER TABLE auditor.asset_copy_history
535 ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
537 ALTER TABLE auditor.serial_unit_history
538 ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
540 ALTER TABLE config.copy_status
541 ADD COLUMN copy_active BOOL NOT NULL DEFAULT FALSE;
543 ALTER TABLE config.circ_matrix_weights
544 ADD COLUMN item_age NUMERIC(6,2) NOT NULL DEFAULT 0.0;
546 ALTER TABLE config.hold_matrix_weights
547 ADD COLUMN item_age NUMERIC(6,2) NOT NULL DEFAULT 0.0;
549 -- The two defaults above were to stop erroring on NOT NULL
551 ALTER TABLE config.circ_matrix_weights
552 ALTER COLUMN item_age DROP DEFAULT;
554 ALTER TABLE config.hold_matrix_weights
555 ALTER COLUMN item_age DROP DEFAULT;
557 ALTER TABLE config.circ_matrix_matchpoint
558 ADD COLUMN item_age INTERVAL;
560 ALTER TABLE config.hold_matrix_matchpoint
561 ADD COLUMN item_age INTERVAL;
563 --Removed dupe asset.acp_status_changed
565 CREATE OR REPLACE FUNCTION asset.acp_created()
566 RETURNS TRIGGER AS $$
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();
571 IF NEW.status_changed_time IS NULL THEN
572 NEW.status_changed_time := now();
578 CREATE TRIGGER acp_created_trig
579 BEFORE INSERT ON asset.copy
580 FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
582 CREATE TRIGGER sunit_created_trig
583 BEFORE INSERT ON serial.unit
584 FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
586 --Removed dupe action.hold_request_permit_test
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$
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;
596 my_item_age INTERVAL;
597 denominator NUMERIC(6,2);
599 result action.found_circ_matrix_matchpoint;
602 result.success = false;
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;
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);
614 SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
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)
625 -- No weights? Bad admin! Defaults to handle that anyway.
626 IF weights.id IS NULL THEN
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;
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
652 SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
654 SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
656 -- Loop over all the potential matchpoints
657 FOR cur_matchpoint IN
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
667 -- AND (m.grp IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
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)
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))
689 CASE WHEN upgad.distance IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
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"
715 -- Record the full matching row list
716 row_list := row_list || cur_matchpoint.id;
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.
725 -- Incomplete matchpoint?
726 IF matchpoint.circulate IS NULL THEN
727 matchpoint.circulate := cur_matchpoint.circulate;
729 IF matchpoint.duration_rule IS NULL THEN
730 matchpoint.duration_rule := cur_matchpoint.duration_rule;
732 IF matchpoint.recurring_fine_rule IS NULL THEN
733 matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
735 IF matchpoint.max_fine_rule IS NULL THEN
736 matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
738 IF matchpoint.hard_due_date IS NULL THEN
739 matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
741 IF matchpoint.total_copy_hold_ratio IS NULL THEN
742 matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
744 IF matchpoint.available_copy_hold_ratio IS NULL THEN
745 matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
747 IF matchpoint.renewals IS NULL THEN
748 matchpoint.renewals := cur_matchpoint.renewals;
750 IF matchpoint.grace_period IS NULL THEN
751 matchpoint.grace_period := cur_matchpoint.grace_period;
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;
764 -- Include the assembled matchpoint, even if it isn't complete
765 result.matchpoint := matchpoint;
767 -- Include (for debugging) the full list of matching rows
768 result.buildrows := row_list;
770 -- Hand the result back to caller
773 $func$ LANGUAGE plpgsql;
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)
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);
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;
795 SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
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;
802 -- Grab the closest set circ weight setting.
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)
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)
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;
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
849 SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
851 SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
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;
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;
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
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)
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))
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 +
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"
922 -- Return just the ID for now
923 RETURN matchpoint.id;
925 $func$ LANGUAGE 'plpgsql';
927 DROP INDEX IF EXISTS config.ccmm_once_per_paramset;
929 DROP INDEX IF EXISTS config.chmm_once_per_paramset;
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;
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;
935 UPDATE config.copy_status SET copy_active = true WHERE id IN (0, 1, 7, 8, 10, 12, 15);
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');
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);
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);
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;
950 -- Evergreen DB patch 0564.data.delete_empty_volume.sql
952 -- New org setting cat.volume.delete_on_empty
955 -- check whether patch can be applied
956 SELECT evergreen.upgrade_deps_block_check('0564', :eg_version);
958 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype )
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'),
967 -- Evergreen DB patch 0565.schema.action-trigger.event_definition.hold-cancel-no-target-notification.sql
969 -- New action trigger event definition: Hold Cancelled (No Target) Email Notification
972 -- check whether patch can be applied
973 SELECT evergreen.upgrade_deps_block_check('0565', :eg_version);
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',
982 [%- user = target.0.usr -%]
983 To: [%- params.recipient_email || user.email %]
984 From: [%- params.sender_email || default_sender %]
985 Subject: Hold Request Cancelled
987 Dear [% user.family_name %], [% user.first_given_name %]
988 The following holds were cancelled because no items were found to fullfil the hold.
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') %]
999 INSERT INTO action_trigger.environment (event_def, path) VALUES
1002 (38, 'bib_rec.bib_record.simple_record');
1004 -- Evergreen DB patch XXXX.data.ou_setting_generate_overdue_on_lost.sql.sql
1006 -- check whether patch can be applied
1007 SELECT evergreen.upgrade_deps_block_check('0567', :eg_version);
1009 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
1010 'circ.lost.generate_overdue_on_checkin',
1012 'circ.lost.generate_overdue_on_checkin',
1013 'Circ: Lost Checkin Generates New Overdues',
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',
1026 -- Evergreen DB patch 0572.vandelay-record-matching-and-quality.sql
1030 -- check whether patch can be applied
1031 SELECT evergreen.upgrade_deps_block_check('0572', :eg_version);
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;
1035 CREATE TABLE vandelay.match_set (
1036 id SERIAL PRIMARY KEY,
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)
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),
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)
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,
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))
1073 CREATE UNIQUE INDEX vmsq_def_once_per_set ON vandelay.match_set_quality (match_set, COALESCE(tag,''), COALESCE(subfield,''), COALESCE(svf,''), value);
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;
1081 CREATE TABLE vandelay.import_error (
1082 code TEXT PRIMARY KEY,
1083 description TEXT NOT NULL -- i18n
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;
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;
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;
1102 ALTER TABLE vandelay.merge_profile ADD COLUMN lwm_ratio NUMERIC;
1104 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
1111 retval config.marc21_rec_type_map%ROWTYPE;
1113 ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
1115 IF ldr IS NULL OR ldr = '' THEN
1116 SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
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
1124 tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1125 bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1127 -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1129 SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1132 IF retval.code IS NULL THEN
1133 SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1138 $func$ LANGUAGE PLPGSQL;
1140 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
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 );
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 );
1161 val := REPEAT( ff_pos.default_val, ff_pos.length );
1167 $func$ LANGUAGE PLPGSQL;
1169 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
1174 output biblio.record_ff_map%ROWTYPE;
1176 rtype := (vandelay.marc21_record_type( marc )).code;
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;
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 );
1187 output.ff_value := NULL;
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;
1194 output.ff_value := NULL;
1202 $func$ LANGUAGE PLPGSQL;
1204 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
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;
1214 _007 := oils_xpath_string( '//*[@tag="007"]', marc );
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 );
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 );
1223 IF pval.id IS NOT NULL THEN
1226 retval.ptype := ptype.ptype_key;
1227 retval.subfield := psf.id;
1228 retval.value := pval.id;
1238 $func$ LANGUAGE PLPGSQL;
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$
1244 use MARC::File::XML (BinaryEncoding => 'UTF-8');
1248 MARC::Charset->assume_unicode(1);
1251 my $r = MARC::Record->new_from_xml( $xml );
1253 return_next( { tag => 'LDR', value => $r->leader } );
1255 for my $f ( $r->fields ) {
1256 if ($f->is_control_field) {
1257 return_next({ tag => $f->tag, value => $f->data });
1259 for my $s ($f->subfields) {
1262 ind1 => $f->indicator(1),
1263 ind2 => $f->indicator(2),
1264 subfield => $s->[0],
1268 if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
1269 my $trim = $f->indicator(2) || 0;
1272 ind1 => $f->indicator(1),
1273 ind2 => $f->indicator(2),
1275 value => substr( $s->[1], $trim )
1284 $func$ LANGUAGE PLPERLU;
1286 CREATE OR REPLACE FUNCTION vandelay.flatten_marc ( marc TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
1288 output vandelay.flat_marc%ROWTYPE;
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);
1299 output.value := field.value;
1302 CONTINUE WHEN output.value IS NULL;
1307 $func$ LANGUAGE PLPGSQL;
1309 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT, attr_defs TEXT[]) RETURNS hstore AS $_$
1311 transformed_xml TEXT;
1314 xfrm config.xml_transform%ROWTYPE;
1316 new_attrs HSTORE := ''::HSTORE;
1317 attr_def config.record_attr_definition%ROWTYPE;
1320 FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE name IN (SELECT * FROM UNNEST(attr_defs)) ORDER BY format LOOP
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
1327 WHEN attr_def.sf_list IS NOT NULL
1328 THEN POSITION(x.subfield IN attr_def.sf_list) > 0
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);
1338 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1340 SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
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);
1348 transformed_xml := xml;
1351 prev_xfrm := xfrm.name;
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;
1360 attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
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 ...
1371 -- apply index normalizers to attr_value
1373 SELECT n.func AS func,
1374 n.param_count AS param_count,
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
1380 EXECUTE 'SELECT ' || normalizer.func || '(' ||
1381 quote_literal( attr_value ) ||
1383 WHEN normalizer.param_count > 0
1384 THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1387 ')' INTO attr_value;
1391 -- Add the new value to the hstore
1392 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
1398 $_$ LANGUAGE PLPGSQL;
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));
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);
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 $$
1421 tags_rstore := vandelay.flatten_marc_hstore(record_xml);
1422 svf_rstore := vandelay.extract_rec_attrs(record_xml);
1424 CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
1425 CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
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);
1431 query_ := 'SELECT bre.id AS record, ';
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;
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 ';
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;
1447 -- add those joins and the where clause to our query.
1448 query_ := query_ || joins || E'\n' || 'WHERE ' || wq || ' AND not bre.deleted';
1450 -- this will return rows of record,quality
1451 FOR rec IN EXECUTE query_ USING tags_rstore, svf_rstore LOOP
1455 DROP TABLE _vandelay_tmp_qrows;
1456 DROP TABLE _vandelay_tmp_jrows;
1460 $$ LANGUAGE PLPGSQL;
1462 CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
1464 ) RETURNS HSTORE AS $$
1468 ARRAY_ACCUM(tag || (COALESCE(subfield, ''))),
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
1478 $$ LANGUAGE PLPGSQL;
1480 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set(
1481 match_set_id INTEGER
1482 ) RETURNS TEXT AS $$
1484 root vandelay.match_set_point;
1486 SELECT * INTO root FROM vandelay.match_set_point
1487 WHERE parent IS NULL AND match_set = match_set_id;
1489 RETURN vandelay.get_expr_from_match_set_point(root);
1491 $$ LANGUAGE PLPGSQL;
1493 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set_point(
1494 node vandelay.match_set_point
1495 ) RETURNS TEXT AS $$
1501 child vandelay.match_set_point;
1503 SELECT ARRAY_ACCUM(id) INTO children FROM vandelay.match_set_point
1504 WHERE parent = node.id;
1506 IF ARRAY_LENGTH(children, 1) > 0 THEN
1507 this_op := vandelay._get_expr_render_one(node);
1510 WHILE children[i] IS NOT NULL LOOP
1511 SELECT * INTO child FROM vandelay.match_set_point
1512 WHERE id = children[i];
1514 q := q || ' ' || this_op || ' ';
1517 q := q || vandelay.get_expr_from_match_set_point(child);
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);
1529 $$ LANGUAGE PLPGSQL;
1531 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_qrow(
1532 node vandelay.match_set_point
1533 ) RETURNS VOID AS $$
1536 INSERT INTO _vandelay_tmp_qrows (q) VALUES (node.id);
1538 $$ LANGUAGE PLPGSQL;
1540 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_jrow(
1541 node vandelay.match_set_point
1542 ) RETURNS VOID AS $$
1555 IF node.tag IS NOT NULL THEN
1557 IF node.subfield IS NOT NULL THEN
1558 tagkey := tagkey || node.subfield;
1562 my_alias := 'n' || node.id::TEXT;
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 = ''' ||
1570 IF node.subfield IS NOT NULL THEN
1571 jrow := jrow || ' AND ' || my_alias || '.subfield = ''' ||
1572 node.subfield || '''';
1574 jrow := jrow || ' AND (' || my_alias || '.value ' || op ||
1575 ' ANY(($1->''' || tagkey || ''')::TEXT[])))';
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 || '''))';
1582 INSERT INTO _vandelay_tmp_jrows (j) VALUES (jrow);
1584 $$ LANGUAGE PLPGSQL;
1586 CREATE OR REPLACE FUNCTION vandelay._get_expr_render_one(
1587 node vandelay.match_set_point
1588 ) RETURNS TEXT AS $$
1592 IF node.bool_op IS NOT NULL THEN
1593 RETURN node.bool_op;
1595 RETURN '(n' || node.id::TEXT || '.id IS NOT NULL)';
1598 $$ LANGUAGE PLPGSQL;
1600 CREATE OR REPLACE FUNCTION vandelay.match_bib_record() RETURNS TRIGGER AS $func$
1602 incoming_existing_id TEXT;
1603 test_result vandelay.match_set_test_result%ROWTYPE;
1607 IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1611 DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
1613 SELECT q.match_set INTO match_set FROM vandelay.bib_queue q WHERE q.id = NEW.queue;
1615 IF match_set IS NOT NULL THEN
1616 NEW.quality := vandelay.measure_record_quality( NEW.marc, match_set );
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);
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)
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;
1638 IF match_set IS NULL THEN
1642 FOR test_result IN SELECT * FROM
1643 vandelay.match_set_test_marcxml(match_set, NEW.marc) LOOP
1645 INSERT INTO vandelay.bib_match ( queued_record, eg_record, match_score, quality )
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;
1658 $func$ LANGUAGE PLPGSQL;
1660 CREATE OR REPLACE FUNCTION vandelay.measure_record_quality ( xml TEXT, match_set_id INT ) RETURNS INT AS $_$
1664 test vandelay.match_set_quality%ROWTYPE;
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;
1675 IF test.value = vandelay.extract_rec_attrs(xml, ARRAY[test.svf]) -> test.svf THEN
1676 out_q := out_q + test.quality;
1683 $_$ LANGUAGE PLPGSQL;
1686 CREATE OR REPLACE FUNCTION vandelay.overlay_bib_record ( import_id BIGINT, eg_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1688 merge_profile vandelay.merge_profile%ROWTYPE;
1689 dyn_profile vandelay.compile_profile%ROWTYPE;
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)
1704 IF v_marc IS NULL THEN
1705 -- RAISE NOTICE 'no marc for vandelay or bib record';
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,
1713 WHERE id = import_id;
1715 editor_string := (oils_xpath('//*[@tag="905"]/*[@code="u"]/text()',v_marc))[1];
1717 IF editor_string IS NOT NULL AND editor_string <> '' THEN
1718 SELECT usr INTO editor_id FROM actor.card WHERE barcode = editor_string;
1720 IF editor_id IS NULL THEN
1721 SELECT id INTO editor_id FROM actor.usr WHERE usrname = editor_string;
1724 IF editor_id IS NOT NULL THEN
1725 UPDATE biblio.record_entry SET editor = editor_id WHERE id = eg_id;
1732 -- RAISE NOTICE 'update of biblio.record_entry failed';
1737 $$ LANGUAGE PLPGSQL;
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 $$
1743 lwm_ratio_value NUMERIC;
1746 lwm_ratio_value := COALESCE(lwm_ratio_value_p, 0.0);
1748 PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
1751 -- RAISE NOTICE 'already imported, cannot auto-overlay'
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
1767 IF eg_id IS NULL THEN
1768 -- RAISE NOTICE 'incoming record is not of high enough quality';
1772 RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
1774 $$ LANGUAGE PLPGSQL;
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 $$
1779 lwm_ratio_value NUMERIC;
1782 lwm_ratio_value := COALESCE(lwm_ratio_value_p, 0.0);
1784 PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
1787 -- RAISE NOTICE 'already imported, cannot auto-overlay'
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
1803 IF eg_id IS NULL THEN
1804 -- RAISE NOTICE 'incoming record is not of high enough quality';
1808 RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
1810 $$ LANGUAGE PLPGSQL;
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 $$
1815 queued_record vandelay.queued_bib_record%ROWTYPE;
1818 FOR queued_record IN SELECT * FROM vandelay.queued_bib_record WHERE queue = queue_id AND import_time IS NULL LOOP
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;
1829 $$ LANGUAGE PLPGSQL;
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;
1835 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_marc ( ) RETURNS TRIGGER AS $$
1841 IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1845 FOR adef IN SELECT * FROM vandelay.bib_attr_definition LOOP
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);
1856 $$ LANGUAGE PLPGSQL;
1858 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_items ( ) RETURNS TRIGGER AS $func$
1861 item_data vandelay.import_item%ROWTYPE;
1864 IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1868 SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
1870 FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
1871 INSERT INTO vandelay.import_item (
1895 item_data.definition,
1896 item_data.owning_lib,
1898 item_data.call_number,
1899 item_data.copy_number,
1902 item_data.circulate,
1904 item_data.deposit_amount,
1909 item_data.circ_modifier,
1910 item_data.circ_as_type,
1911 item_data.alert_message,
1913 item_data.priv_note,
1914 item_data.opac_visible
1920 $func$ LANGUAGE PLPGSQL;
1922 CREATE OR REPLACE FUNCTION vandelay.cleanup_bib_marc ( ) RETURNS TRIGGER AS $$
1924 IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1928 DELETE FROM vandelay.queued_bib_record_attr WHERE record = OLD.id;
1929 DELETE FROM vandelay.import_item WHERE record = OLD.id;
1931 IF TG_OP = 'UPDATE' THEN
1936 $$ LANGUAGE PLPGSQL;
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();
1945 CREATE OR REPLACE FUNCTION vandelay.ingest_authority_marc ( ) RETURNS TRIGGER AS $$
1951 IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1955 FOR adef IN SELECT * FROM vandelay.authority_attr_definition LOOP
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);
1966 $$ LANGUAGE PLPGSQL;
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;
1973 ALTER TABLE vandelay.authority_match DROP COLUMN matched_attr;
1975 CREATE OR REPLACE FUNCTION vandelay.cleanup_authority_marc ( ) RETURNS TRIGGER AS $$
1977 IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1981 DELETE FROM vandelay.queued_authority_record_attr WHERE record = OLD.id;
1982 IF TG_OP = 'UPDATE' THEN
1987 $$ LANGUAGE PLPGSQL;
1989 CREATE OR REPLACE FUNCTION authority.flatten_marc ( rid BIGINT ) RETURNS SETOF authority.full_rec AS $func$
1991 auth authority.record_entry%ROWTYPE;
1992 output authority.full_rec%ROWTYPE;
1995 SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
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;
2008 $func$ LANGUAGE PLPGSQL;
2010 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
2012 bib biblio.record_entry%ROWTYPE;
2013 output metabib.full_rec%ROWTYPE;
2016 SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
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;
2029 $func$ LANGUAGE PLPGSQL;
2031 -----------------------------------------------
2032 -- Seed data for import errors
2033 -----------------------------------------------
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') );
2048 ----------------------------------------------------------------
2049 -- Seed data for queued record/item exports
2050 ----------------------------------------------------------------
2052 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES (
2053 'vandelay.queued_bib_record.print',
2056 'vandelay.queued_bib_record.print',
2057 'Print output has been requested for records in an Importer Bib Queue.',
2064 'vandelay.queued_bib_record.csv',
2067 'vandelay.queued_bib_record.csv',
2068 'CSV output has been requested for records in an Importer Bib Queue.',
2075 'vandelay.queued_bib_record.email',
2078 'vandelay.queued_bib_record.email',
2079 'An email has been requested for records in an Importer Bib Queue.',
2086 'vandelay.queued_auth_record.print',
2089 'vandelay.queued_auth_record.print',
2090 'Print output has been requested for records in an Importer Authority Queue.',
2097 'vandelay.queued_auth_record.csv',
2100 'vandelay.queued_auth_record.csv',
2101 'CSV output has been requested for records in an Importer Authority Queue.',
2108 'vandelay.queued_auth_record.email',
2111 'vandelay.queued_auth_record.email',
2112 'An email has been requested for records in an Importer Authority Queue.',
2119 'vandelay.import_items.print',
2122 'vandelay.import_items.print',
2123 'Print output has been requested for Import Items from records in an Importer Bib Queue.',
2130 'vandelay.import_items.csv',
2133 'vandelay.import_items.csv',
2134 'CSV output has been requested for Import Items from records in an Importer Bib Queue.',
2141 'vandelay.import_items.email',
2144 'vandelay.import_items.email',
2145 'An email has been requested for Import Items from records in an Importer Bib Queue.',
2153 INSERT INTO action_trigger.event_definition (
2168 'Print Output for Queued Bib Records',
2169 'vandelay.queued_bib_record.print',
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 %]
2182 [% FOR vqbr IN target %]
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) %]
2206 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2211 INSERT INTO action_trigger.event_definition (
2226 'CSV Output for Queued Bib Records',
2227 'vandelay.queued_bib_record.csv',
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('"', '""') %]"
2241 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2246 INSERT INTO action_trigger.event_definition (
2261 'Email Output for Queued Bib Records',
2262 'vandelay.queued_bib_record.email',
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
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 %]
2279 [% FOR vqbr IN target %]
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) %]
2303 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2306 ,( 41, 'queue.owner')
2309 INSERT INTO action_trigger.event_definition (
2324 'Print Output for Queued Authority Records',
2325 'vandelay.queued_auth_record.print',
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 %]
2338 [% FOR vqar IN target %]
2340 Record Identifier | [% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) %]
2348 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2353 INSERT INTO action_trigger.event_definition (
2368 'CSV Output for Queued Authority Records',
2369 'vandelay.queued_auth_record.csv',
2377 [% FOR vqar IN target %]"[% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) | replace('"', '""') %]"
2383 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2388 INSERT INTO action_trigger.event_definition (
2403 'Email Output for Queued Authority Records',
2404 'vandelay.queued_auth_record.email',
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
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 %]
2421 [% FOR vqar IN target %]
2423 Record Identifier | [% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) %]
2431 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2434 ,( 44, 'queue.owner')
2437 INSERT INTO action_trigger.event_definition (
2452 'Print Output for Import Items from Queued Bib Records',
2453 'vandelay.import_items.print',
2456 'record.queue.owner',
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 %]
2466 [% FOR vii IN target %]
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 %]
2500 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2502 ,( 45, 'record.attributes')
2503 ,( 45, 'record.queue')
2504 ,( 45, 'record.queue.owner')
2507 INSERT INTO action_trigger.event_definition (
2522 'CSV Output for Import Items from Queued Bib Records',
2523 'vandelay.import_items.csv',
2526 'record.queue.owner',
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('"', '""') %]"
2537 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2539 ,( 46, 'record.attributes')
2540 ,( 46, 'record.queue')
2541 ,( 46, 'record.queue.owner')
2544 INSERT INTO action_trigger.event_definition (
2559 'Email Output for Import Items from Queued Bib Records',
2560 'vandelay.import_items.email',
2563 'record.queue.owner',
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
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 %]
2577 [% FOR vii IN target %]
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 %]
2610 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2612 ,( 47, 'record.attributes')
2613 ,( 47, 'record.queue')
2614 ,( 47, 'record.queue.owner')
2619 SELECT evergreen.upgrade_deps_block_check('0574', :eg_version);
2621 UPDATE action_trigger.event_definition SET template =
2625 table { border-collapse: collapse; }
2626 td { padding: 5px; border-bottom: 1px solid #888; }
2627 th { font-weight: bold; }
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 = [];
2634 SET current_location = target.0.current_copy.location.id;
2636 IF current_location != hold.current_copy.location.id;
2637 SET current_location = hold.current_copy.location.id;
2638 holds_list.push(loc_data);
2643 'callnumber' => hold.current_copy.call_number.label
2645 loc_data.push(hold_data);
2647 holds_list.push(loc_data)
2654 <th>Shelving Location</th>
2655 <th>Call Number</th>
2656 <th>Barcode/Part</th>
2661 [% FOR loc_data IN holds_list %]
2662 [% FOR hold_data IN loc_data.sort('callnumber') %]
2664 SET hold = hold_data.hold;
2665 SET copy_data = helpers.get_copy_bib_basics(hold.current_copy.id);
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 %]
2677 <td>[% hold.usr.card.barcode %]</td>
2686 INSERT INTO action_trigger.environment (
2690 (35, 'current_copy.parts'),
2691 (35, 'current_copy.parts.part')
2695 -- Evergreen DB patch XXXX.schema.authority-control-sets.sql
2697 -- Schema upgrade to add Authority Control Set functionality
2701 -- check whether patch can be applied
2702 SELECT evergreen.upgrade_deps_block_check('0575', :eg_version);
2704 CREATE TABLE authority.control_set (
2705 id SERIAL PRIMARY KEY,
2706 name TEXT NOT NULL UNIQUE, -- i18n
2707 description TEXT -- i18n
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,
2716 sf_list TEXT NOT NULL,
2717 name TEXT NOT NULL, -- i18n
2718 description TEXT -- i18n
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
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
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,
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
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;
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);
2753 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT, no_thesaurus BOOL ) RETURNS TEXT AS $func$
2755 acsaf authority.control_set_authority_field%ROWTYPE;
2763 thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
2764 IF thes_code IS NULL THEN
2768 SELECT control_set INTO cset FROM authority.thesaurus WHERE code = thes_code;
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;
2782 EXIT WHEN heading_text <> '';
2785 IF thes_code = 'z' THEN
2786 thes_code := oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml);
2789 IF heading_text <> '' THEN
2790 IF no_thesaurus IS TRUE THEN
2791 heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
2793 heading_text := tag_used || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
2796 heading_text := 'NOHEADING_' || thes_code || ' ' || MD5(marcxml);
2799 RETURN heading_text;
2801 $func$ LANGUAGE PLPGSQL IMMUTABLE;
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;
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;
2811 CREATE OR REPLACE VIEW authority.tracing_links AS
2812 SELECT main.record AS record,
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,
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' );
2837 CREATE OR REPLACE FUNCTION authority.generate_overlay_template (source_xml TEXT) RETURNS TEXT AS $f$
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[];
2847 IF auth_id IS NULL THEN
2851 -- Default to the LoC controll set
2852 SELECT COALESCE(control_set,1) INTO cset FROM authority.record_entry WHERE id = auth_id;
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 || '$]' );
2867 XMLATTRIBUTES('http://www.loc.gov/MARC21/slim' AS xmlns),
2868 XMLELEMENT( name leader, '00881nam a2200193 4500'),
2872 XMLATTRIBUTES( '905' AS tag, ' ' AS ind1, ' ' AS ind2),
2875 XMLATTRIBUTES('r' AS code),
2876 ARRAY_TO_STRING(replace_rules,',')
2881 $f$ STABLE LANGUAGE PLPGSQL;
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;
2887 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
2890 use MARC::File::XML (BinaryEncoding => 'UTF-8');
2894 MARC::Charset->assume_unicode(1);
2896 my $target_xml = shift;
2897 my $source_xml = shift;
2898 my $field_spec = shift;
2899 my $force_add = shift || 0;
2901 my $target_r = MARC::Record->new_from_xml( $target_xml );
2902 my $source_r = MARC::Record->new_from_xml( $source_xml );
2904 return $target_xml unless ($target_r && $source_r);
2906 my @field_list = split(',', $field_spec);
2909 for my $f (@field_list) {
2910 $f =~ s/^\s*//; $f =~ s/\s*$//;
2911 if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
2917 $match =~ s/^\s*//; $match =~ s/\s*$//;
2918 $fields{$field} = { sf => [ split('', $sf) ] };
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/ };
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 );
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 );
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}));
2943 my @new_sf = map { ($_ => $from_field->subfield($_)) } grep { defined($from_field->subfield($_)) } @{$fields{$f}{sf}};
2944 $to_field->add_subfields( @new_sf );
2949 my @new_fields = map { $_->clone } $source_r->field( $f );
2950 $target_r->insert_fields_ordered( @new_fields );
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;
2961 $_$ LANGUAGE PLPERLU;
2964 CREATE INDEX by_heading ON authority.record_entry (authority.simple_normalize_heading(marc)) WHERE deleted IS FALSE or deleted = FALSE;
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);
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',' ');
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');
2976 INSERT INTO config.metabib_field_index_norm_map (field,norm,pos)
2980 FROM config.metabib_field m,
2981 config.index_normalizer i
2982 WHERE i.func = 'remove_paren_substring'
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);
2989 INSERT INTO authority.control_set (id, name, description) VALUES (
2991 oils_i18n_gettext('1','LoC','acs','name'),
2992 oils_i18n_gettext('1','Library of Congress standard authority record control semantics','acs','description')
2995 INSERT INTO authority.control_set_authority_field (id, control_set, main_entry, tag, sf_list, name) VALUES
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')),
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')),