]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/999.functions.global.sql
LP 2061136 follow-up: ng lint --fix
[Evergreen.git] / Open-ILS / src / sql / Pg / 999.functions.global.sql
1 /*
2  * Copyright (C) 2008 Equinox Software, Inc.
3  * Bill Erickson <erickson@esilibrary.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  */
16
17 CREATE OR REPLACE FUNCTION actor.usr_merge_rows( table_name TEXT, col_name TEXT, src_usr INT, dest_usr INT ) RETURNS VOID AS $$
18 DECLARE
19     sel TEXT;
20     upd TEXT;
21     del TEXT;
22     cur_row RECORD;
23 BEGIN
24     sel := 'SELECT id::BIGINT FROM ' || table_name || ' WHERE ' || quote_ident(col_name) || ' = ' || quote_literal(src_usr);
25     upd := 'UPDATE ' || table_name || ' SET ' || quote_ident(col_name) || ' = ' || quote_literal(dest_usr) || ' WHERE id = ';
26     del := 'DELETE FROM ' || table_name || ' WHERE id = ';
27     FOR cur_row IN EXECUTE sel LOOP
28         BEGIN
29             --RAISE NOTICE 'Attempting to merge % %', table_name, cur_row.id;
30             EXECUTE upd || cur_row.id;
31         EXCEPTION WHEN unique_violation THEN
32             --RAISE NOTICE 'Deleting conflicting % %', table_name, cur_row.id;
33             EXECUTE del || cur_row.id;
34         END;
35     END LOOP;
36 END;
37 $$ LANGUAGE plpgsql;
38
39 COMMENT ON FUNCTION actor.usr_merge_rows(TEXT, TEXT, INT, INT) IS $$
40 Attempts to move each row of the specified table from src_user to dest_user.  
41 Where conflicts exist, the conflicting "source" row is deleted.
42 $$;
43
44
45 CREATE OR REPLACE FUNCTION actor.usr_merge( src_usr INT, dest_usr INT, del_addrs BOOLEAN, del_cards BOOLEAN, deactivate_cards BOOLEAN ) RETURNS VOID AS $$
46 DECLARE
47         suffix TEXT;
48         bucket_row RECORD;
49         picklist_row RECORD;
50         queue_row RECORD;
51         folder_row RECORD;
52 BEGIN
53
54     -- Bail if src_usr equals dest_usr because the result of merging a
55     -- user with itself is not what you want.
56     IF src_usr = dest_usr THEN
57         RETURN;
58     END IF;
59
60     -- do some initial cleanup 
61     UPDATE actor.usr SET card = NULL WHERE id = src_usr;
62     UPDATE actor.usr SET mailing_address = NULL WHERE id = src_usr;
63     UPDATE actor.usr SET billing_address = NULL WHERE id = src_usr;
64
65     -- actor.*
66     IF del_cards THEN
67         DELETE FROM actor.card where usr = src_usr;
68     ELSE
69         IF deactivate_cards THEN
70             UPDATE actor.card SET active = 'f' WHERE usr = src_usr;
71         END IF;
72         UPDATE actor.card SET usr = dest_usr WHERE usr = src_usr;
73     END IF;
74
75
76     IF del_addrs THEN
77         DELETE FROM actor.usr_address WHERE usr = src_usr;
78     ELSE
79         UPDATE actor.usr_address SET usr = dest_usr WHERE usr = src_usr;
80     END IF;
81
82     UPDATE actor.usr_message SET usr = dest_usr WHERE usr = src_usr;
83     -- dupes are technically OK in actor.usr_standing_penalty, should manually delete them...
84     UPDATE actor.usr_standing_penalty SET usr = dest_usr WHERE usr = src_usr;
85     PERFORM actor.usr_merge_rows('actor.usr_org_unit_opt_in', 'usr', src_usr, dest_usr);
86     PERFORM actor.usr_merge_rows('actor.usr_setting', 'usr', src_usr, dest_usr);
87
88     -- permission.*
89     PERFORM actor.usr_merge_rows('permission.usr_perm_map', 'usr', src_usr, dest_usr);
90     PERFORM actor.usr_merge_rows('permission.usr_object_perm_map', 'usr', src_usr, dest_usr);
91     PERFORM actor.usr_merge_rows('permission.usr_grp_map', 'usr', src_usr, dest_usr);
92     PERFORM actor.usr_merge_rows('permission.usr_work_ou_map', 'usr', src_usr, dest_usr);
93
94
95     -- container.*
96         
97         -- For each *_bucket table: transfer every bucket belonging to src_usr
98         -- into the custody of dest_usr.
99         --
100         -- In order to avoid colliding with an existing bucket owned by
101         -- the destination user, append the source user's id (in parenthesese)
102         -- to the name.  If you still get a collision, add successive
103         -- spaces to the name and keep trying until you succeed.
104         --
105         FOR bucket_row in
106                 SELECT id, name
107                 FROM   container.biblio_record_entry_bucket
108                 WHERE  owner = src_usr
109         LOOP
110                 suffix := ' (' || src_usr || ')';
111                 LOOP
112                         BEGIN
113                                 UPDATE  container.biblio_record_entry_bucket
114                                 SET     owner = dest_usr, name = name || suffix
115                                 WHERE   id = bucket_row.id;
116                         EXCEPTION WHEN unique_violation THEN
117                                 suffix := suffix || ' ';
118                                 CONTINUE;
119                         END;
120                         EXIT;
121                 END LOOP;
122         END LOOP;
123
124         FOR bucket_row in
125                 SELECT id, name
126                 FROM   container.call_number_bucket
127                 WHERE  owner = src_usr
128         LOOP
129                 suffix := ' (' || src_usr || ')';
130                 LOOP
131                         BEGIN
132                                 UPDATE  container.call_number_bucket
133                                 SET     owner = dest_usr, name = name || suffix
134                                 WHERE   id = bucket_row.id;
135                         EXCEPTION WHEN unique_violation THEN
136                                 suffix := suffix || ' ';
137                                 CONTINUE;
138                         END;
139                         EXIT;
140                 END LOOP;
141         END LOOP;
142
143         FOR bucket_row in
144                 SELECT id, name
145                 FROM   container.copy_bucket
146                 WHERE  owner = src_usr
147         LOOP
148                 suffix := ' (' || src_usr || ')';
149                 LOOP
150                         BEGIN
151                                 UPDATE  container.copy_bucket
152                                 SET     owner = dest_usr, name = name || suffix
153                                 WHERE   id = bucket_row.id;
154                         EXCEPTION WHEN unique_violation THEN
155                                 suffix := suffix || ' ';
156                                 CONTINUE;
157                         END;
158                         EXIT;
159                 END LOOP;
160         END LOOP;
161
162         FOR bucket_row in
163                 SELECT id, name
164                 FROM   container.user_bucket
165                 WHERE  owner = src_usr
166         LOOP
167                 suffix := ' (' || src_usr || ')';
168                 LOOP
169                         BEGIN
170                                 UPDATE  container.user_bucket
171                                 SET     owner = dest_usr, name = name || suffix
172                                 WHERE   id = bucket_row.id;
173                         EXCEPTION WHEN unique_violation THEN
174                                 suffix := suffix || ' ';
175                                 CONTINUE;
176                         END;
177                         EXIT;
178                 END LOOP;
179         END LOOP;
180
181         UPDATE container.user_bucket_item SET target_user = dest_usr WHERE target_user = src_usr;
182
183     -- vandelay.*
184         -- transfer queues the same way we transfer buckets (see above)
185         FOR queue_row in
186                 SELECT id, name
187                 FROM   vandelay.queue
188                 WHERE  owner = src_usr
189         LOOP
190                 suffix := ' (' || src_usr || ')';
191                 LOOP
192                         BEGIN
193                                 UPDATE  vandelay.queue
194                                 SET     owner = dest_usr, name = name || suffix
195                                 WHERE   id = queue_row.id;
196                         EXCEPTION WHEN unique_violation THEN
197                                 suffix := suffix || ' ';
198                                 CONTINUE;
199                         END;
200                         EXIT;
201                 END LOOP;
202         END LOOP;
203
204     UPDATE vandelay.session_tracker SET usr = dest_usr WHERE usr = src_usr;
205
206     -- money.*
207     PERFORM actor.usr_merge_rows('money.collections_tracker', 'usr', src_usr, dest_usr);
208     PERFORM actor.usr_merge_rows('money.collections_tracker', 'collector', src_usr, dest_usr);
209     UPDATE money.billable_xact SET usr = dest_usr WHERE usr = src_usr;
210     UPDATE money.billing SET voider = dest_usr WHERE voider = src_usr;
211     UPDATE money.bnm_payment SET accepting_usr = dest_usr WHERE accepting_usr = src_usr;
212
213     -- action.*
214     UPDATE action.circulation SET usr = dest_usr WHERE usr = src_usr;
215     UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
216     UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
217     UPDATE action.usr_circ_history SET usr = dest_usr WHERE usr = src_usr;
218
219     UPDATE action.hold_request SET usr = dest_usr WHERE usr = src_usr;
220     UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
221     UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
222     UPDATE action.hold_request SET canceled_by = dest_usr WHERE canceled_by = src_usr;
223     UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
224
225     UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
226     UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
227     UPDATE action.non_cataloged_circulation SET patron = dest_usr WHERE patron = src_usr;
228     UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
229     UPDATE action.survey_response SET usr = dest_usr WHERE usr = src_usr;
230
231     -- acq.*
232     UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
233         UPDATE acq.fund_transfer SET transfer_user = dest_usr WHERE transfer_user = src_usr;
234     UPDATE acq.invoice SET closed_by = dest_usr WHERE closed_by = src_usr;
235
236         -- transfer picklists the same way we transfer buckets (see above)
237         FOR picklist_row in
238                 SELECT id, name
239                 FROM   acq.picklist
240                 WHERE  owner = src_usr
241         LOOP
242                 suffix := ' (' || src_usr || ')';
243                 LOOP
244                         BEGIN
245                                 UPDATE  acq.picklist
246                                 SET     owner = dest_usr, name = name || suffix
247                                 WHERE   id = picklist_row.id;
248                         EXCEPTION WHEN unique_violation THEN
249                                 suffix := suffix || ' ';
250                                 CONTINUE;
251                         END;
252                         EXIT;
253                 END LOOP;
254         END LOOP;
255
256     UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
257     UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
258     UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
259     UPDATE acq.provider_note SET creator = dest_usr WHERE creator = src_usr;
260     UPDATE acq.provider_note SET editor = dest_usr WHERE editor = src_usr;
261     UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
262     UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
263     UPDATE acq.lineitem_usr_attr_definition SET usr = dest_usr WHERE usr = src_usr;
264
265     -- asset.*
266     UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
267     UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
268     UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
269     UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
270     UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
271     UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
272
273     -- serial.*
274     UPDATE serial.record_entry SET creator = dest_usr WHERE creator = src_usr;
275     UPDATE serial.record_entry SET editor = dest_usr WHERE editor = src_usr;
276
277     -- reporter.*
278     -- It's not uncommon to define the reporter schema in a replica 
279     -- DB only, so don't assume these tables exist in the write DB.
280     BEGIN
281         UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
282     EXCEPTION WHEN undefined_table THEN
283         -- do nothing
284     END;
285     BEGIN
286         UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
287     EXCEPTION WHEN undefined_table THEN
288         -- do nothing
289     END;
290     BEGIN
291         UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
292     EXCEPTION WHEN undefined_table THEN
293         -- do nothing
294     END;
295     BEGIN
296                 -- transfer folders the same way we transfer buckets (see above)
297                 FOR folder_row in
298                         SELECT id, name
299                         FROM   reporter.template_folder
300                         WHERE  owner = src_usr
301                 LOOP
302                         suffix := ' (' || src_usr || ')';
303                         LOOP
304                                 BEGIN
305                                         UPDATE  reporter.template_folder
306                                         SET     owner = dest_usr, name = name || suffix
307                                         WHERE   id = folder_row.id;
308                                 EXCEPTION WHEN unique_violation THEN
309                                         suffix := suffix || ' ';
310                                         CONTINUE;
311                                 END;
312                                 EXIT;
313                         END LOOP;
314                 END LOOP;
315     EXCEPTION WHEN undefined_table THEN
316         -- do nothing
317     END;
318     BEGIN
319                 -- transfer folders the same way we transfer buckets (see above)
320                 FOR folder_row in
321                         SELECT id, name
322                         FROM   reporter.report_folder
323                         WHERE  owner = src_usr
324                 LOOP
325                         suffix := ' (' || src_usr || ')';
326                         LOOP
327                                 BEGIN
328                                         UPDATE  reporter.report_folder
329                                         SET     owner = dest_usr, name = name || suffix
330                                         WHERE   id = folder_row.id;
331                                 EXCEPTION WHEN unique_violation THEN
332                                         suffix := suffix || ' ';
333                                         CONTINUE;
334                                 END;
335                                 EXIT;
336                         END LOOP;
337                 END LOOP;
338     EXCEPTION WHEN undefined_table THEN
339         -- do nothing
340     END;
341     BEGIN
342                 -- transfer folders the same way we transfer buckets (see above)
343                 FOR folder_row in
344                         SELECT id, name
345                         FROM   reporter.output_folder
346                         WHERE  owner = src_usr
347                 LOOP
348                         suffix := ' (' || src_usr || ')';
349                         LOOP
350                                 BEGIN
351                                         UPDATE  reporter.output_folder
352                                         SET     owner = dest_usr, name = name || suffix
353                                         WHERE   id = folder_row.id;
354                                 EXCEPTION WHEN unique_violation THEN
355                                         suffix := suffix || ' ';
356                                         CONTINUE;
357                                 END;
358                                 EXIT;
359                         END LOOP;
360                 END LOOP;
361     EXCEPTION WHEN undefined_table THEN
362         -- do nothing
363     END;
364
365     -- propagate preferred name values from the source user to the
366     -- destination user, but only when values are not being replaced.
367     WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr)
368     UPDATE actor.usr SET 
369         pref_prefix = 
370             COALESCE(pref_prefix, (SELECT pref_prefix FROM susr)),
371         pref_first_given_name = 
372             COALESCE(pref_first_given_name, (SELECT pref_first_given_name FROM susr)),
373         pref_second_given_name = 
374             COALESCE(pref_second_given_name, (SELECT pref_second_given_name FROM susr)),
375         pref_family_name = 
376             COALESCE(pref_family_name, (SELECT pref_family_name FROM susr)),
377         pref_suffix = 
378             COALESCE(pref_suffix, (SELECT pref_suffix FROM susr))
379     WHERE id = dest_usr;
380
381     -- Copy and deduplicate name keywords
382     -- String -> array -> rows -> DISTINCT -> array -> string
383     WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr),
384          dusr AS (SELECT * FROM actor.usr WHERE id = dest_usr)
385     UPDATE actor.usr SET name_keywords = (
386         WITH keywords AS (
387             SELECT DISTINCT UNNEST(
388                 REGEXP_SPLIT_TO_ARRAY(
389                     COALESCE((SELECT name_keywords FROM susr), '') || ' ' ||
390                     COALESCE((SELECT name_keywords FROM dusr), ''),  E'\\s+'
391                 )
392             ) AS parts
393         ) SELECT STRING_AGG(kw.parts, ' ') FROM keywords kw
394     ) WHERE id = dest_usr;
395
396     -- Finally, delete the source user
397     PERFORM actor.usr_delete(src_usr,dest_usr);
398
399 END;
400 $$ LANGUAGE plpgsql;
401
402
403
404 COMMENT ON FUNCTION actor.usr_merge(INT, INT, BOOLEAN, BOOLEAN, BOOLEAN) IS $$
405 Merges all user date from src_usr to dest_usr.  When collisions occur, 
406 keep dest_usr's data and delete src_usr's data.
407 $$;
408
409
410 CREATE OR REPLACE FUNCTION actor.usr_purge_data(
411         src_usr  IN INTEGER,
412         specified_dest_usr IN INTEGER
413 ) RETURNS VOID AS $$
414 DECLARE
415         suffix TEXT;
416         renamable_row RECORD;
417         dest_usr INTEGER;
418 BEGIN
419
420         IF specified_dest_usr IS NULL THEN
421                 dest_usr := 1; -- Admin user on stock installs
422         ELSE
423                 dest_usr := specified_dest_usr;
424         END IF;
425
426     -- action_trigger.event (even doing this, event_output may--and probably does--contain PII and should have a retention/removal policy)
427     UPDATE action_trigger.event SET context_user = dest_usr WHERE context_user = src_usr;
428
429         -- acq.*
430         UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
431         UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
432         UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
433         UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
434         UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
435         UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
436         UPDATE acq.invoice SET closed_by = dest_usr WHERE closed_by = src_usr;
437         DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
438
439         -- Update with a rename to avoid collisions
440         FOR renamable_row in
441                 SELECT id, name
442                 FROM   acq.picklist
443                 WHERE  owner = src_usr
444         LOOP
445                 suffix := ' (' || src_usr || ')';
446                 LOOP
447                         BEGIN
448                                 UPDATE  acq.picklist
449                                 SET     owner = dest_usr, name = name || suffix
450                                 WHERE   id = renamable_row.id;
451                         EXCEPTION WHEN unique_violation THEN
452                                 suffix := suffix || ' ';
453                                 CONTINUE;
454                         END;
455                         EXIT;
456                 END LOOP;
457         END LOOP;
458
459         UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
460         UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
461         UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
462         UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
463         UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
464         UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
465         UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
466         UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
467
468         -- action.*
469         DELETE FROM action.circulation WHERE usr = src_usr;
470         UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
471         UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
472         UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
473         UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
474         UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
475         UPDATE action.hold_request SET canceled_by = dest_usr WHERE canceled_by = src_usr;
476         DELETE FROM action.hold_request WHERE usr = src_usr;
477         UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
478         UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
479         DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
480         UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
481         DELETE FROM action.survey_response WHERE usr = src_usr;
482         UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
483         DELETE FROM action.usr_circ_history WHERE usr = src_usr;
484         UPDATE action.curbside SET notes = NULL WHERE patron = src_usr;
485
486         -- actor.*
487         DELETE FROM actor.card WHERE usr = src_usr;
488         DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
489         DELETE FROM actor.usr_privacy_waiver WHERE usr = src_usr;
490         DELETE FROM actor.usr_message WHERE usr = src_usr;
491
492         -- The following update is intended to avoid transient violations of a foreign
493         -- key constraint, whereby actor.usr_address references itself.  It may not be
494         -- necessary, but it does no harm.
495         UPDATE actor.usr_address SET replaces = NULL
496                 WHERE usr = src_usr AND replaces IS NOT NULL;
497         DELETE FROM actor.usr_address WHERE usr = src_usr;
498         DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
499         UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
500         DELETE FROM actor.usr_setting WHERE usr = src_usr;
501         DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
502         UPDATE actor.usr_message SET title = 'purged', message = 'purged', read_date = NOW() WHERE usr = src_usr;
503         DELETE FROM actor.usr_message WHERE usr = src_usr;
504         UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
505         UPDATE actor.usr_message SET editor = dest_usr WHERE editor = src_usr;
506
507         -- asset.*
508         UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
509         UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
510         UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
511         UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
512         UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
513         UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
514
515         -- auditor.*
516         DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
517         DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
518         UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
519         UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
520         UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
521         UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
522         UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
523         UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
524
525         -- biblio.*
526         UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
527         UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
528         UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
529         UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
530
531         -- container.*
532         -- Update buckets with a rename to avoid collisions
533         FOR renamable_row in
534                 SELECT id, name
535                 FROM   container.biblio_record_entry_bucket
536                 WHERE  owner = src_usr
537         LOOP
538                 suffix := ' (' || src_usr || ')';
539                 LOOP
540                         BEGIN
541                                 UPDATE  container.biblio_record_entry_bucket
542                                 SET     owner = dest_usr, name = name || suffix
543                                 WHERE   id = renamable_row.id;
544                         EXCEPTION WHEN unique_violation THEN
545                                 suffix := suffix || ' ';
546                                 CONTINUE;
547                         END;
548                         EXIT;
549                 END LOOP;
550         END LOOP;
551
552         FOR renamable_row in
553                 SELECT id, name
554                 FROM   container.call_number_bucket
555                 WHERE  owner = src_usr
556         LOOP
557                 suffix := ' (' || src_usr || ')';
558                 LOOP
559                         BEGIN
560                                 UPDATE  container.call_number_bucket
561                                 SET     owner = dest_usr, name = name || suffix
562                                 WHERE   id = renamable_row.id;
563                         EXCEPTION WHEN unique_violation THEN
564                                 suffix := suffix || ' ';
565                                 CONTINUE;
566                         END;
567                         EXIT;
568                 END LOOP;
569         END LOOP;
570
571         FOR renamable_row in
572                 SELECT id, name
573                 FROM   container.copy_bucket
574                 WHERE  owner = src_usr
575         LOOP
576                 suffix := ' (' || src_usr || ')';
577                 LOOP
578                         BEGIN
579                                 UPDATE  container.copy_bucket
580                                 SET     owner = dest_usr, name = name || suffix
581                                 WHERE   id = renamable_row.id;
582                         EXCEPTION WHEN unique_violation THEN
583                                 suffix := suffix || ' ';
584                                 CONTINUE;
585                         END;
586                         EXIT;
587                 END LOOP;
588         END LOOP;
589
590         FOR renamable_row in
591                 SELECT id, name
592                 FROM   container.user_bucket
593                 WHERE  owner = src_usr
594         LOOP
595                 suffix := ' (' || src_usr || ')';
596                 LOOP
597                         BEGIN
598                                 UPDATE  container.user_bucket
599                                 SET     owner = dest_usr, name = name || suffix
600                                 WHERE   id = renamable_row.id;
601                         EXCEPTION WHEN unique_violation THEN
602                                 suffix := suffix || ' ';
603                                 CONTINUE;
604                         END;
605                         EXIT;
606                 END LOOP;
607         END LOOP;
608
609         DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
610
611         -- money.*
612         DELETE FROM money.billable_xact WHERE usr = src_usr;
613         DELETE FROM money.collections_tracker WHERE usr = src_usr;
614         UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
615
616         -- permission.*
617         DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
618         DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
619         DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
620         DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
621
622         -- reporter.*
623         -- Update with a rename to avoid collisions
624         BEGIN
625                 FOR renamable_row in
626                         SELECT id, name
627                         FROM   reporter.output_folder
628                         WHERE  owner = src_usr
629                 LOOP
630                         suffix := ' (' || src_usr || ')';
631                         LOOP
632                                 BEGIN
633                                         UPDATE  reporter.output_folder
634                                         SET     owner = dest_usr, name = name || suffix
635                                         WHERE   id = renamable_row.id;
636                                 EXCEPTION WHEN unique_violation THEN
637                                         suffix := suffix || ' ';
638                                         CONTINUE;
639                                 END;
640                                 EXIT;
641                         END LOOP;
642                 END LOOP;
643         EXCEPTION WHEN undefined_table THEN
644                 -- do nothing
645         END;
646
647         BEGIN
648                 UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
649         EXCEPTION WHEN undefined_table THEN
650                 -- do nothing
651         END;
652
653         -- Update with a rename to avoid collisions
654         BEGIN
655                 FOR renamable_row in
656                         SELECT id, name
657                         FROM   reporter.report_folder
658                         WHERE  owner = src_usr
659                 LOOP
660                         suffix := ' (' || src_usr || ')';
661                         LOOP
662                                 BEGIN
663                                         UPDATE  reporter.report_folder
664                                         SET     owner = dest_usr, name = name || suffix
665                                         WHERE   id = renamable_row.id;
666                                 EXCEPTION WHEN unique_violation THEN
667                                         suffix := suffix || ' ';
668                                         CONTINUE;
669                                 END;
670                                 EXIT;
671                         END LOOP;
672                 END LOOP;
673         EXCEPTION WHEN undefined_table THEN
674                 -- do nothing
675         END;
676
677         BEGIN
678                 UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
679         EXCEPTION WHEN undefined_table THEN
680                 -- do nothing
681         END;
682
683         BEGIN
684                 UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
685         EXCEPTION WHEN undefined_table THEN
686                 -- do nothing
687         END;
688
689         -- Update with a rename to avoid collisions
690         BEGIN
691                 FOR renamable_row in
692                         SELECT id, name
693                         FROM   reporter.template_folder
694                         WHERE  owner = src_usr
695                 LOOP
696                         suffix := ' (' || src_usr || ')';
697                         LOOP
698                                 BEGIN
699                                         UPDATE  reporter.template_folder
700                                         SET     owner = dest_usr, name = name || suffix
701                                         WHERE   id = renamable_row.id;
702                                 EXCEPTION WHEN unique_violation THEN
703                                         suffix := suffix || ' ';
704                                         CONTINUE;
705                                 END;
706                                 EXIT;
707                         END LOOP;
708                 END LOOP;
709         EXCEPTION WHEN undefined_table THEN
710         -- do nothing
711         END;
712
713         -- vandelay.*
714         -- Update with a rename to avoid collisions
715         FOR renamable_row in
716                 SELECT id, name
717                 FROM   vandelay.queue
718                 WHERE  owner = src_usr
719         LOOP
720                 suffix := ' (' || src_usr || ')';
721                 LOOP
722                         BEGIN
723                                 UPDATE  vandelay.queue
724                                 SET     owner = dest_usr, name = name || suffix
725                                 WHERE   id = renamable_row.id;
726                         EXCEPTION WHEN unique_violation THEN
727                                 suffix := suffix || ' ';
728                                 CONTINUE;
729                         END;
730                         EXIT;
731                 END LOOP;
732         END LOOP;
733
734     UPDATE vandelay.session_tracker SET usr = dest_usr WHERE usr = src_usr;
735
736     -- NULL-ify addresses last so other cleanup (e.g. circ anonymization)
737     -- can access the information before deletion.
738         UPDATE actor.usr SET
739                 active = FALSE,
740                 card = NULL,
741                 mailing_address = NULL,
742                 billing_address = NULL
743         WHERE id = src_usr;
744
745 END;
746 $$ LANGUAGE plpgsql;
747
748 COMMENT ON FUNCTION actor.usr_purge_data(INT, INT) IS $$
749 Finds rows dependent on a given row in actor.usr and either deletes them
750 or reassigns them to a different user.
751 $$;
752
753
754
755 CREATE OR REPLACE FUNCTION actor.usr_delete(
756         src_usr  IN INTEGER,
757         dest_usr IN INTEGER
758 ) RETURNS VOID AS $$
759 DECLARE
760         old_profile actor.usr.profile%type;
761         old_home_ou actor.usr.home_ou%type;
762         new_profile actor.usr.profile%type;
763         new_home_ou actor.usr.home_ou%type;
764         new_name    text;
765         new_dob     actor.usr.dob%type;
766 BEGIN
767         SELECT
768                 id || '-PURGED-' || now(),
769                 profile,
770                 home_ou,
771                 dob
772         INTO
773                 new_name,
774                 old_profile,
775                 old_home_ou,
776                 new_dob
777         FROM
778                 actor.usr
779         WHERE
780                 id = src_usr;
781         --
782         -- Quit if no such user
783         --
784         IF old_profile IS NULL THEN
785                 RETURN;
786         END IF;
787         --
788         perform actor.usr_purge_data( src_usr, dest_usr );
789         --
790         -- Find the root grp_tree and the root org_unit.  This would be simpler if we 
791         -- could assume that there is only one root.  Theoretically, someday, maybe,
792         -- there could be multiple roots, so we take extra trouble to get the right ones.
793         --
794         SELECT
795                 id
796         INTO
797                 new_profile
798         FROM
799                 permission.grp_ancestors( old_profile )
800         WHERE
801                 parent is null;
802         --
803         SELECT
804                 id
805         INTO
806                 new_home_ou
807         FROM
808                 actor.org_unit_ancestors( old_home_ou )
809         WHERE
810                 parent_ou is null;
811         --
812         -- Truncate date of birth
813         --
814         IF new_dob IS NOT NULL THEN
815                 new_dob := date_trunc( 'year', new_dob );
816         END IF;
817         --
818         UPDATE
819                 actor.usr
820                 SET
821                         card = NULL,
822                         profile = new_profile,
823                         usrname = new_name,
824                         email = NULL,
825                         passwd = random()::text,
826                         standing = DEFAULT,
827                         ident_type = 
828                         (
829                                 SELECT MIN( id )
830                                 FROM config.identification_type
831                         ),
832                         ident_value = NULL,
833                         ident_type2 = NULL,
834                         ident_value2 = NULL,
835                         net_access_level = DEFAULT,
836                         photo_url = NULL,
837                         prefix = NULL,
838                         first_given_name = new_name,
839                         second_given_name = NULL,
840                         family_name = new_name,
841                         suffix = NULL,
842                         alias = NULL,
843             guardian = NULL,
844                         day_phone = NULL,
845                         evening_phone = NULL,
846                         other_phone = NULL,
847                         mailing_address = NULL,
848                         billing_address = NULL,
849                         home_ou = new_home_ou,
850                         dob = new_dob,
851                         active = FALSE,
852                         master_account = DEFAULT, 
853                         super_user = DEFAULT,
854                         barred = FALSE,
855                         deleted = TRUE,
856                         juvenile = DEFAULT,
857                         usrgroup = 0,
858                         claims_returned_count = DEFAULT,
859                         credit_forward_balance = DEFAULT,
860                         last_xact_id = DEFAULT,
861                         pref_prefix = NULL,
862                         pref_first_given_name = NULL,
863                         pref_second_given_name = NULL,
864                         pref_family_name = NULL,
865                         pref_suffix = NULL,
866                         name_keywords = NULL,
867                         create_date = now(),
868                         expire_date = now()
869         WHERE
870                 id = src_usr;
871 END;
872 $$ LANGUAGE plpgsql;
873
874 COMMENT ON FUNCTION actor.usr_delete(INT, INT) IS $$
875 Logically deletes a user.  Removes personally identifiable information,
876 and purges associated data in other tables.
877 $$;
878
879
880
881 CREATE OR REPLACE FUNCTION actor.approve_pending_address(pending_id INT) RETURNS BIGINT AS $$
882 DECLARE
883     old_id INT;
884 BEGIN
885     SELECT INTO old_id replaces FROM actor.usr_address where id = pending_id;
886     IF old_id IS NULL THEN
887         UPDATE actor.usr_address SET pending = 'f' WHERE id = pending_id;
888         RETURN pending_id;
889     END IF;
890     -- address replaces an existing address
891     DELETE FROM actor.usr_address WHERE id = -old_id;
892     UPDATE actor.usr_address SET id = -id WHERE id = old_id;
893     UPDATE actor.usr_address SET replaces = NULL, id = old_id, pending = 'f' WHERE id = pending_id;
894     RETURN old_id;
895 END
896 $$ LANGUAGE plpgsql;
897
898 COMMENT ON FUNCTION actor.approve_pending_address(INT) IS $$
899 Replaces an address with a pending address.  This is done by giving the pending 
900 address the ID of the old address.  The replaced address is retained with -id.
901 $$;
902
903 CREATE OR REPLACE FUNCTION actor.change_password (user_id INT, new_pw TEXT, pw_type TEXT DEFAULT 'main')
904 RETURNS VOID AS $$
905 DECLARE
906     new_salt TEXT;
907 BEGIN
908     SELECT actor.create_salt(pw_type) INTO new_salt;
909
910     IF pw_type = 'main' THEN
911         -- Only 'main' passwords are required to have
912         -- the extra layer of MD5 hashing.
913         PERFORM actor.set_passwd(
914             user_id, pw_type, md5(new_salt || md5(new_pw)), new_salt
915         );
916
917     ELSE
918         PERFORM actor.set_passwd(user_id, pw_type, new_pw, new_salt);
919     END IF;
920 END;
921 $$ LANGUAGE 'plpgsql';
922
923 COMMENT ON FUNCTION actor.change_password(INT,TEXT,TEXT) IS $$
924 Allows setting a salted password for a user by passing actor.usr id and the text of the password.
925 $$;
926
927 CREATE OR REPLACE FUNCTION container.clear_expired_circ_history_items( 
928          ac_usr IN INTEGER
929 ) RETURNS VOID AS $$
930 --
931 -- Delete old circulation bucket items for a specified user.
932 -- "Old" means older than the interval specified by a
933 -- user-level setting, if it is so specified.
934 --
935 DECLARE
936     threshold TIMESTAMP WITH TIME ZONE;
937 BEGIN
938         -- Sanity check
939         IF ac_usr IS NULL THEN
940                 RETURN;
941         END IF;
942         -- Determine the threshold date that defines "old".  Subtract the
943         -- interval from the system date, then truncate to midnight.
944         SELECT
945                 date_trunc( 
946                         'day',
947                         now() - CAST( translate( value, '"', '' ) AS INTERVAL )
948                 )
949         INTO
950                 threshold
951         FROM
952                 actor.usr_setting
953         WHERE
954                 usr = ac_usr
955                 AND name = 'patron.max_reading_list_interval';
956         --
957         IF threshold is null THEN
958                 -- No interval defined; don't delete anything
959                 -- RAISE NOTICE 'No interval defined for user %', ac_usr;
960                 return;
961         END IF;
962         --
963         -- RAISE NOTICE 'Date threshold: %', threshold;
964         --
965         -- Threshold found; do the delete
966         delete from container.copy_bucket_item
967         where
968                 bucket in
969                 (
970                         select
971                                 id
972                         from
973                                 container.copy_bucket
974                         where
975                                 owner = ac_usr
976                                 and btype = 'circ_history'
977                 )
978                 and create_time < threshold;
979         --
980         RETURN;
981 END;
982 $$ LANGUAGE plpgsql;
983
984 COMMENT ON FUNCTION container.clear_expired_circ_history_items( INTEGER ) IS $$
985 Delete old circulation bucket items for a specified user.
986 "Old" means older than the interval specified by a
987 user-level setting, if it is so specified.
988 $$;
989
990 CREATE OR REPLACE FUNCTION container.clear_all_expired_circ_history_items( )
991 RETURNS VOID AS $$
992 --
993 -- Delete expired circulation bucket items for all users that have
994 -- a setting for patron.max_reading_list_interval.
995 --
996 DECLARE
997     today        TIMESTAMP WITH TIME ZONE;
998     threshold    TIMESTAMP WITH TIME ZONE;
999         usr_setting  RECORD;
1000 BEGIN
1001         SELECT date_trunc( 'day', now() ) INTO today;
1002         --
1003         FOR usr_setting in
1004                 SELECT
1005                         usr,
1006                         value
1007                 FROM
1008                         actor.usr_setting
1009                 WHERE
1010                         name = 'patron.max_reading_list_interval'
1011         LOOP
1012                 --
1013                 -- Make sure the setting is a valid interval
1014                 --
1015                 BEGIN
1016                         threshold := today - CAST( translate( usr_setting.value, '"', '' ) AS INTERVAL );
1017                 EXCEPTION
1018                         WHEN OTHERS THEN
1019                                 RAISE NOTICE 'Invalid setting patron.max_reading_list_interval for user %: ''%''',
1020                                         usr_setting.usr, usr_setting.value;
1021                                 CONTINUE;
1022                 END;
1023                 --
1024                 --RAISE NOTICE 'User % threshold %', usr_setting.usr, threshold;
1025                 --
1026         DELETE FROM container.copy_bucket_item
1027         WHERE
1028                 bucket IN
1029                 (
1030                     SELECT
1031                         id
1032                     FROM
1033                         container.copy_bucket
1034                     WHERE
1035                         owner = usr_setting.usr
1036                         AND btype = 'circ_history'
1037                 )
1038                 AND create_time < threshold;
1039         END LOOP;
1040         --
1041 END;
1042 $$ LANGUAGE plpgsql;
1043
1044 COMMENT ON FUNCTION container.clear_all_expired_circ_history_items( ) IS $$
1045 Delete expired circulation bucket items for all users that have
1046 a setting for patron.max_reading_list_interval.
1047 $$;
1048
1049 CREATE OR REPLACE FUNCTION asset.merge_record_assets( target_record BIGINT, source_record BIGINT ) RETURNS INT AS $func$
1050 DECLARE
1051     moved_objects INT := 0;
1052     source_cn     asset.call_number%ROWTYPE;
1053     target_cn     asset.call_number%ROWTYPE;
1054     metarec       metabib.metarecord%ROWTYPE;
1055     hold          action.hold_request%ROWTYPE;
1056     ser_rec       serial.record_entry%ROWTYPE;
1057     ser_sub       serial.subscription%ROWTYPE;
1058     acq_lineitem  acq.lineitem%ROWTYPE;
1059     acq_request   acq.user_request%ROWTYPE;
1060     booking       booking.resource_type%ROWTYPE;
1061     source_part   biblio.monograph_part%ROWTYPE;
1062     target_part   biblio.monograph_part%ROWTYPE;
1063     multi_home    biblio.peer_bib_copy_map%ROWTYPE;
1064     uri_count     INT := 0;
1065     counter       INT := 0;
1066     uri_datafield TEXT;
1067     uri_text      TEXT := '';
1068 BEGIN
1069
1070     -- we don't merge bib -1
1071     IF target_record = -1 OR source_record = -1 THEN
1072        RETURN 0;
1073     END IF;
1074
1075     -- move any 856 entries on records that have at least one MARC-mapped URI entry
1076     SELECT  INTO uri_count COUNT(*)
1077       FROM  asset.uri_call_number_map m
1078             JOIN asset.call_number cn ON (m.call_number = cn.id)
1079       WHERE cn.record = source_record;
1080
1081     IF uri_count > 0 THEN
1082         
1083         -- This returns more nodes than you might expect:
1084         -- 7 instead of 1 for an 856 with $u $y $9
1085         SELECT  COUNT(*) INTO counter
1086           FROM  oils_xpath_table(
1087                     'id',
1088                     'marc',
1089                     'biblio.record_entry',
1090                     '//*[@tag="856"]',
1091                     'id=' || source_record
1092                 ) as t(i int,c text);
1093     
1094         FOR i IN 1 .. counter LOOP
1095             SELECT  '<datafield xmlns="http://www.loc.gov/MARC21/slim"' || 
1096                         ' tag="856"' ||
1097                         ' ind1="' || FIRST(ind1) || '"'  ||
1098                         ' ind2="' || FIRST(ind2) || '">' ||
1099                         STRING_AGG(
1100                             '<subfield code="' || subfield || '">' ||
1101                             regexp_replace(
1102                                 regexp_replace(
1103                                     regexp_replace(data,'&','&amp;','g'),
1104                                     '>', '&gt;', 'g'
1105                                 ),
1106                                 '<', '&lt;', 'g'
1107                             ) || '</subfield>', ''
1108                         ) || '</datafield>' INTO uri_datafield
1109               FROM  oils_xpath_table(
1110                         'id',
1111                         'marc',
1112                         'biblio.record_entry',
1113                         '//*[@tag="856"][position()=' || i || ']/@ind1|' ||
1114                         '//*[@tag="856"][position()=' || i || ']/@ind2|' ||
1115                         '//*[@tag="856"][position()=' || i || ']/*/@code|' ||
1116                         '//*[@tag="856"][position()=' || i || ']/*[@code]',
1117                         'id=' || source_record
1118                     ) as t(id int,ind1 text, ind2 text,subfield text,data text);
1119
1120             -- As most of the results will be NULL, protect against NULLifying
1121             -- the valid content that we do generate
1122             uri_text := uri_text || COALESCE(uri_datafield, '');
1123         END LOOP;
1124
1125         IF uri_text <> '' THEN
1126             UPDATE  biblio.record_entry
1127               SET   marc = regexp_replace(marc,'(</[^>]*record>)', uri_text || E'\\1')
1128               WHERE id = target_record;
1129         END IF;
1130
1131     END IF;
1132
1133         -- Find and move metarecords to the target record
1134         SELECT  INTO metarec *
1135           FROM  metabib.metarecord
1136           WHERE master_record = source_record;
1137
1138         IF FOUND THEN
1139                 UPDATE  metabib.metarecord
1140                   SET   master_record = target_record,
1141                         mods = NULL
1142                   WHERE id = metarec.id;
1143
1144                 moved_objects := moved_objects + 1;
1145         END IF;
1146
1147         -- Find call numbers attached to the source ...
1148         FOR source_cn IN SELECT * FROM asset.call_number WHERE record = source_record LOOP
1149
1150                 SELECT  INTO target_cn *
1151                   FROM  asset.call_number
1152                   WHERE label = source_cn.label
1153             AND prefix = source_cn.prefix
1154             AND suffix = source_cn.suffix
1155                         AND owning_lib = source_cn.owning_lib
1156                         AND record = target_record
1157                         AND NOT deleted;
1158
1159                 -- ... and if there's a conflicting one on the target ...
1160                 IF FOUND THEN
1161
1162                         -- ... move the copies to that, and ...
1163                         UPDATE  asset.copy
1164                           SET   call_number = target_cn.id
1165                           WHERE call_number = source_cn.id;
1166
1167                         -- ... move V holds to the move-target call number
1168                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_cn.id AND hold_type = 'V' LOOP
1169                 
1170                                 UPDATE  action.hold_request
1171                                   SET   target = target_cn.id
1172                                   WHERE id = hold.id;
1173                 
1174                                 moved_objects := moved_objects + 1;
1175                         END LOOP;
1176         
1177             UPDATE asset.call_number SET deleted = TRUE WHERE id = source_cn.id;
1178
1179                 -- ... if not ...
1180                 ELSE
1181                         -- ... just move the call number to the target record
1182                         UPDATE  asset.call_number
1183                           SET   record = target_record
1184                           WHERE id = source_cn.id;
1185                 END IF;
1186
1187                 moved_objects := moved_objects + 1;
1188         END LOOP;
1189
1190         -- Find T holds targeting the source record ...
1191         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_record AND hold_type = 'T' LOOP
1192
1193                 -- ... and move them to the target record
1194                 UPDATE  action.hold_request
1195                   SET   target = target_record
1196                   WHERE id = hold.id;
1197
1198                 moved_objects := moved_objects + 1;
1199         END LOOP;
1200
1201         -- Find serial records targeting the source record ...
1202         FOR ser_rec IN SELECT * FROM serial.record_entry WHERE record = source_record LOOP
1203                 -- ... and move them to the target record
1204                 UPDATE  serial.record_entry
1205                   SET   record = target_record
1206                   WHERE id = ser_rec.id;
1207
1208                 moved_objects := moved_objects + 1;
1209         END LOOP;
1210
1211         -- Find serial subscriptions targeting the source record ...
1212         FOR ser_sub IN SELECT * FROM serial.subscription WHERE record_entry = source_record LOOP
1213                 -- ... and move them to the target record
1214                 UPDATE  serial.subscription
1215                   SET   record_entry = target_record
1216                   WHERE id = ser_sub.id;
1217
1218                 moved_objects := moved_objects + 1;
1219         END LOOP;
1220
1221         -- Find booking resource types targeting the source record ...
1222         FOR booking IN SELECT * FROM booking.resource_type WHERE record = source_record LOOP
1223                 -- ... and move them to the target record
1224                 UPDATE  booking.resource_type
1225                   SET   record = target_record
1226                   WHERE id = booking.id;
1227
1228                 moved_objects := moved_objects + 1;
1229         END LOOP;
1230
1231         -- Find acq lineitems targeting the source record ...
1232         FOR acq_lineitem IN SELECT * FROM acq.lineitem WHERE eg_bib_id = source_record LOOP
1233                 -- ... and move them to the target record
1234                 UPDATE  acq.lineitem
1235                   SET   eg_bib_id = target_record
1236                   WHERE id = acq_lineitem.id;
1237
1238                 moved_objects := moved_objects + 1;
1239         END LOOP;
1240
1241         -- Find acq user purchase requests targeting the source record ...
1242         FOR acq_request IN SELECT * FROM acq.user_request WHERE eg_bib = source_record LOOP
1243                 -- ... and move them to the target record
1244                 UPDATE  acq.user_request
1245                   SET   eg_bib = target_record
1246                   WHERE id = acq_request.id;
1247
1248                 moved_objects := moved_objects + 1;
1249         END LOOP;
1250
1251         -- Find parts attached to the source ...
1252         FOR source_part IN SELECT * FROM biblio.monograph_part WHERE record = source_record LOOP
1253
1254                 SELECT  INTO target_part *
1255                   FROM  biblio.monograph_part
1256                   WHERE label = source_part.label
1257                         AND record = target_record;
1258
1259                 -- ... and if there's a conflicting one on the target ...
1260                 IF FOUND THEN
1261
1262                         -- ... move the copy-part maps to that, and ...
1263                         UPDATE  asset.copy_part_map
1264                           SET   part = target_part.id
1265                           WHERE part = source_part.id;
1266
1267                         -- ... move P holds to the move-target part
1268                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_part.id AND hold_type = 'P' LOOP
1269                 
1270                                 UPDATE  action.hold_request
1271                                   SET   target = target_part.id
1272                                   WHERE id = hold.id;
1273                 
1274                                 moved_objects := moved_objects + 1;
1275                         END LOOP;
1276
1277                 -- ... if not ...
1278                 ELSE
1279                         -- ... just move the part to the target record
1280                         UPDATE  biblio.monograph_part
1281                           SET   record = target_record
1282                           WHERE id = source_part.id;
1283                 END IF;
1284
1285                 moved_objects := moved_objects + 1;
1286         END LOOP;
1287
1288         -- Find multi_home items attached to the source ...
1289         FOR multi_home IN SELECT * FROM biblio.peer_bib_copy_map WHERE peer_record = source_record LOOP
1290                 -- ... and move them to the target record
1291                 UPDATE  biblio.peer_bib_copy_map
1292                   SET   peer_record = target_record
1293                   WHERE id = multi_home.id;
1294
1295                 moved_objects := moved_objects + 1;
1296         END LOOP;
1297
1298         -- And delete mappings where the item's home bib was merged with the peer bib
1299         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = (
1300                 SELECT (SELECT record FROM asset.call_number WHERE id = call_number)
1301                 FROM asset.copy WHERE id = target_copy
1302         );
1303
1304     -- Apply merge tracking
1305     UPDATE biblio.record_entry 
1306         SET merge_date = NOW() WHERE id = target_record;
1307
1308     UPDATE biblio.record_entry
1309         SET merge_date = NOW(), merged_to = target_record
1310         WHERE id = source_record;
1311
1312     -- replace book bag entries of source_record with target_record
1313     UPDATE container.biblio_record_entry_bucket_item
1314         SET target_biblio_record_entry = target_record
1315         WHERE bucket IN (SELECT id FROM container.biblio_record_entry_bucket WHERE btype = 'bookbag')
1316         AND target_biblio_record_entry = source_record;
1317
1318     -- move over record notes 
1319     UPDATE biblio.record_note 
1320         SET record = target_record, value = CONCAT(value,'; note merged from ',source_record::TEXT) 
1321         WHERE record = source_record
1322         AND NOT deleted;
1323
1324     -- add note to record merge 
1325     INSERT INTO biblio.record_note (record, value) 
1326         VALUES (target_record,CONCAT('record ',source_record::TEXT,' merged on ',NOW()::TEXT));
1327
1328     -- Finally, "delete" the source record
1329     UPDATE biblio.record_entry SET active = FALSE WHERE id = source_record;
1330     DELETE FROM biblio.record_entry WHERE id = source_record;
1331
1332         -- That's all, folks!
1333         RETURN moved_objects;
1334 END;
1335 $func$ LANGUAGE plpgsql;
1336
1337 -- Authority ingest routines
1338
1339 CREATE OR REPLACE FUNCTION authority.propagate_changes (aid BIGINT) RETURNS SETOF BIGINT AS $func$
1340     SELECT authority.propagate_changes( authority, bib ) FROM authority.bib_linking WHERE authority = $1;
1341 $func$ LANGUAGE SQL;
1342
1343 CREATE OR REPLACE FUNCTION authority.map_thesaurus_to_control_set () RETURNS TRIGGER AS $func$
1344 BEGIN
1345     IF NEW.control_set IS NULL THEN
1346         SELECT  control_set INTO NEW.control_set
1347           FROM  authority.thesaurus
1348           WHERE authority.extract_thesaurus(NEW.marc) = code;
1349     END IF;
1350
1351     RETURN NEW;
1352 END;
1353 $func$ LANGUAGE PLPGSQL;
1354
1355 CREATE OR REPLACE FUNCTION authority.reingest_authority_rec_descriptor( auth_id BIGINT ) RETURNS VOID AS $func$
1356 BEGIN
1357     DELETE FROM authority.rec_descriptor WHERE record = auth_id;
1358     INSERT INTO authority.rec_descriptor (record, record_status, encoding_level, thesaurus)
1359         SELECT  auth_id,
1360                 vandelay.marc21_extract_fixed_field(marc,'RecStat'),
1361                 vandelay.marc21_extract_fixed_field(marc,'ELvl'),
1362                 authority.extract_thesaurus(marc)
1363           FROM  authority.record_entry
1364           WHERE id = auth_id;
1365     RETURN;
1366 END;
1367 $func$ LANGUAGE PLPGSQL;
1368
1369 CREATE OR REPLACE FUNCTION authority.reingest_authority_full_rec( auth_id BIGINT ) RETURNS VOID AS $func$
1370 BEGIN
1371     DELETE FROM authority.full_rec WHERE record = auth_id;
1372     INSERT INTO authority.full_rec (record, tag, ind1, ind2, subfield, value)
1373         SELECT record, tag, ind1, ind2, subfield, value FROM authority.flatten_marc( auth_id );
1374
1375     RETURN;
1376 END;
1377 $func$ LANGUAGE PLPGSQL;
1378
1379 -- Given an authority record's ID, control set ID (if known), and marc::XML,
1380 -- return all links to other authority records in the form of rows that
1381 -- can be inserted into authority.authority_linking.
1382 CREATE OR REPLACE FUNCTION authority.calculate_authority_linking(
1383     rec_id BIGINT, rec_control_set INT, rec_marc_xml XML
1384 ) RETURNS SETOF authority.authority_linking AS $func$
1385 DECLARE
1386     acsaf       authority.control_set_authority_field%ROWTYPE;
1387     link        TEXT;
1388     aal         authority.authority_linking%ROWTYPE;
1389 BEGIN
1390     IF rec_control_set IS NULL THEN
1391         -- No control_set on record?  Guess at one
1392         SELECT control_set INTO rec_control_set
1393             FROM authority.control_set_authority_field
1394             WHERE tag IN (
1395                 SELECT UNNEST(
1396                     XPATH('//*[starts-with(@tag,"1")]/@tag',rec_marc_xml)::TEXT[]
1397                 )
1398             ) LIMIT 1;
1399
1400         IF NOT FOUND THEN
1401             RAISE WARNING 'Could not even guess at control set for authority record %', rec_id;
1402             RETURN;
1403         END IF;
1404     END IF;
1405
1406     aal.source := rec_id;
1407
1408     FOR acsaf IN
1409         SELECT * FROM authority.control_set_authority_field
1410         WHERE control_set = rec_control_set
1411             AND linking_subfield IS NOT NULL
1412             AND main_entry IS NOT NULL
1413     LOOP
1414         -- Loop over the trailing-number contents of all linking subfields
1415         FOR link IN
1416             SELECT  SUBSTRING( x::TEXT, '\d+$' )
1417               FROM  UNNEST(
1418                         XPATH(
1419                             '//*[@tag="'
1420                                 || acsaf.tag
1421                                 || '"]/*[@code="'
1422                                 || acsaf.linking_subfield
1423                                 || '"]/text()',
1424                             rec_marc_xml
1425                         )
1426                     ) x
1427         LOOP
1428
1429             -- Ignore links that are null, malformed, circular, or point to
1430             -- non-existent authority records.
1431             IF link IS NOT NULL AND link::BIGINT <> rec_id THEN
1432                 PERFORM * FROM authority.record_entry WHERE id = link::BIGINT;
1433                 IF FOUND THEN
1434                     aal.target := link::BIGINT;
1435                     aal.field := acsaf.id;
1436                     RETURN NEXT aal;
1437                 END IF;
1438             END IF;
1439         END LOOP;
1440     END LOOP;
1441 END;
1442 $func$ LANGUAGE PLPGSQL;
1443
1444 -- Ingest triggers
1445 CREATE TRIGGER fingerprint_tgr BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE biblio.fingerprint_trigger ('eng','BKS');
1446 CREATE TRIGGER bbb_simple_rec_trigger AFTER INSERT OR UPDATE OR DELETE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE reporter.simple_rec_trigger ();
1447
1448 CREATE TRIGGER map_thesaurus_to_control_set BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE authority.map_thesaurus_to_control_set ();
1449
1450 -- Utility routines, callable via cstore
1451
1452 CREATE OR REPLACE FUNCTION config.interval_to_seconds( interval_val INTERVAL )
1453 RETURNS INTEGER AS $$
1454 BEGIN
1455         RETURN EXTRACT( EPOCH FROM interval_val );
1456 END;
1457 $$ LANGUAGE plpgsql;
1458
1459 CREATE OR REPLACE FUNCTION config.interval_to_seconds( interval_string TEXT )
1460 RETURNS INTEGER AS $$
1461 BEGIN
1462         RETURN config.interval_to_seconds( interval_string::INTERVAL );
1463 END;
1464 $$ LANGUAGE plpgsql;
1465
1466 CREATE OR REPLACE FUNCTION vandelay.ingest_items ( import_id BIGINT, attr_def_id BIGINT ) RETURNS SETOF vandelay.import_item AS $$
1467 DECLARE
1468
1469     owning_lib      TEXT;
1470     circ_lib        TEXT;
1471     call_number     TEXT;
1472     copy_number     TEXT;
1473     status          TEXT;
1474     location        TEXT;
1475     circulate       TEXT;
1476     deposit         TEXT;
1477     deposit_amount  TEXT;
1478     ref             TEXT;
1479     holdable        TEXT;
1480     price           TEXT;
1481     barcode         TEXT;
1482     circ_modifier   TEXT;
1483     circ_as_type    TEXT;
1484     alert_message   TEXT;
1485     opac_visible    TEXT;
1486     pub_note        TEXT;
1487     priv_note       TEXT;
1488     internal_id     TEXT;
1489     stat_cat_data   TEXT;
1490     parts_data      TEXT;
1491
1492     attr_def        RECORD;
1493     tmp_attr_set    RECORD;
1494     attr_set        vandelay.import_item%ROWTYPE;
1495
1496     xpaths          TEXT[];
1497     tmp_str         TEXT;
1498
1499 BEGIN
1500
1501     SELECT * INTO attr_def FROM vandelay.import_item_attr_definition WHERE id = attr_def_id;
1502
1503     IF FOUND THEN
1504
1505         attr_set.definition := attr_def.id;
1506
1507         -- Build the combined XPath
1508
1509         owning_lib :=
1510             CASE
1511                 WHEN attr_def.owning_lib IS NULL THEN 'null()'
1512                 WHEN LENGTH( attr_def.owning_lib ) = 1 THEN '//*[@code="' || attr_def.owning_lib || '"]'
1513                 ELSE '//*' || attr_def.owning_lib
1514             END;
1515
1516         circ_lib :=
1517             CASE
1518                 WHEN attr_def.circ_lib IS NULL THEN 'null()'
1519                 WHEN LENGTH( attr_def.circ_lib ) = 1 THEN '//*[@code="' || attr_def.circ_lib || '"]'
1520                 ELSE '//*' || attr_def.circ_lib
1521             END;
1522
1523         call_number :=
1524             CASE
1525                 WHEN attr_def.call_number IS NULL THEN 'null()'
1526                 WHEN LENGTH( attr_def.call_number ) = 1 THEN '//*[@code="' || attr_def.call_number || '"]'
1527                 ELSE '//*' || attr_def.call_number
1528             END;
1529
1530         copy_number :=
1531             CASE
1532                 WHEN attr_def.copy_number IS NULL THEN 'null()'
1533                 WHEN LENGTH( attr_def.copy_number ) = 1 THEN '//*[@code="' || attr_def.copy_number || '"]'
1534                 ELSE '//*' || attr_def.copy_number
1535             END;
1536
1537         status :=
1538             CASE
1539                 WHEN attr_def.status IS NULL THEN 'null()'
1540                 WHEN LENGTH( attr_def.status ) = 1 THEN '//*[@code="' || attr_def.status || '"]'
1541                 ELSE '//*' || attr_def.status
1542             END;
1543
1544         location :=
1545             CASE
1546                 WHEN attr_def.location IS NULL THEN 'null()'
1547                 WHEN LENGTH( attr_def.location ) = 1 THEN '//*[@code="' || attr_def.location || '"]'
1548                 ELSE '//*' || attr_def.location
1549             END;
1550
1551         circulate :=
1552             CASE
1553                 WHEN attr_def.circulate IS NULL THEN 'null()'
1554                 WHEN LENGTH( attr_def.circulate ) = 1 THEN '//*[@code="' || attr_def.circulate || '"]'
1555                 ELSE '//*' || attr_def.circulate
1556             END;
1557
1558         deposit :=
1559             CASE
1560                 WHEN attr_def.deposit IS NULL THEN 'null()'
1561                 WHEN LENGTH( attr_def.deposit ) = 1 THEN '//*[@code="' || attr_def.deposit || '"]'
1562                 ELSE '//*' || attr_def.deposit
1563             END;
1564
1565         deposit_amount :=
1566             CASE
1567                 WHEN attr_def.deposit_amount IS NULL THEN 'null()'
1568                 WHEN LENGTH( attr_def.deposit_amount ) = 1 THEN '//*[@code="' || attr_def.deposit_amount || '"]'
1569                 ELSE '//*' || attr_def.deposit_amount
1570             END;
1571
1572         ref :=
1573             CASE
1574                 WHEN attr_def.ref IS NULL THEN 'null()'
1575                 WHEN LENGTH( attr_def.ref ) = 1 THEN '//*[@code="' || attr_def.ref || '"]'
1576                 ELSE '//*' || attr_def.ref
1577             END;
1578
1579         holdable :=
1580             CASE
1581                 WHEN attr_def.holdable IS NULL THEN 'null()'
1582                 WHEN LENGTH( attr_def.holdable ) = 1 THEN '//*[@code="' || attr_def.holdable || '"]'
1583                 ELSE '//*' || attr_def.holdable
1584             END;
1585
1586         price :=
1587             CASE
1588                 WHEN attr_def.price IS NULL THEN 'null()'
1589                 WHEN LENGTH( attr_def.price ) = 1 THEN '//*[@code="' || attr_def.price || '"]'
1590                 ELSE '//*' || attr_def.price
1591             END;
1592
1593         barcode :=
1594             CASE
1595                 WHEN attr_def.barcode IS NULL THEN 'null()'
1596                 WHEN LENGTH( attr_def.barcode ) = 1 THEN '//*[@code="' || attr_def.barcode || '"]'
1597                 ELSE '//*' || attr_def.barcode
1598             END;
1599
1600         circ_modifier :=
1601             CASE
1602                 WHEN attr_def.circ_modifier IS NULL THEN 'null()'
1603                 WHEN LENGTH( attr_def.circ_modifier ) = 1 THEN '//*[@code="' || attr_def.circ_modifier || '"]'
1604                 ELSE '//*' || attr_def.circ_modifier
1605             END;
1606
1607         circ_as_type :=
1608             CASE
1609                 WHEN attr_def.circ_as_type IS NULL THEN 'null()'
1610                 WHEN LENGTH( attr_def.circ_as_type ) = 1 THEN '//*[@code="' || attr_def.circ_as_type || '"]'
1611                 ELSE '//*' || attr_def.circ_as_type
1612             END;
1613
1614         alert_message :=
1615             CASE
1616                 WHEN attr_def.alert_message IS NULL THEN 'null()'
1617                 WHEN LENGTH( attr_def.alert_message ) = 1 THEN '//*[@code="' || attr_def.alert_message || '"]'
1618                 ELSE '//*' || attr_def.alert_message
1619             END;
1620
1621         opac_visible :=
1622             CASE
1623                 WHEN attr_def.opac_visible IS NULL THEN 'null()'
1624                 WHEN LENGTH( attr_def.opac_visible ) = 1 THEN '//*[@code="' || attr_def.opac_visible || '"]'
1625                 ELSE '//*' || attr_def.opac_visible
1626             END;
1627
1628         pub_note :=
1629             CASE
1630                 WHEN attr_def.pub_note IS NULL THEN 'null()'
1631                 WHEN LENGTH( attr_def.pub_note ) = 1 THEN '//*[@code="' || attr_def.pub_note || '"]'
1632                 ELSE '//*' || attr_def.pub_note
1633             END;
1634         priv_note :=
1635             CASE
1636                 WHEN attr_def.priv_note IS NULL THEN 'null()'
1637                 WHEN LENGTH( attr_def.priv_note ) = 1 THEN '//*[@code="' || attr_def.priv_note || '"]'
1638                 ELSE '//*' || attr_def.priv_note
1639             END;
1640
1641         internal_id :=
1642             CASE
1643                 WHEN attr_def.internal_id IS NULL THEN 'null()'
1644                 WHEN LENGTH( attr_def.internal_id ) = 1 THEN '//*[@code="' || attr_def.internal_id || '"]'
1645                 ELSE '//*' || attr_def.internal_id
1646             END;
1647
1648         stat_cat_data :=
1649             CASE
1650                 WHEN attr_def.stat_cat_data IS NULL THEN 'null()'
1651                 WHEN LENGTH( attr_def.stat_cat_data ) = 1 THEN '//*[@code="' || attr_def.stat_cat_data || '"]'
1652                 ELSE '//*' || attr_def.stat_cat_data
1653             END;
1654
1655         parts_data :=
1656             CASE
1657                 WHEN attr_def.parts_data IS NULL THEN 'null()'
1658                 WHEN LENGTH( attr_def.parts_data ) = 1 THEN '//*[@code="' || attr_def.parts_data || '"]'
1659                 ELSE '//*' || attr_def.parts_data
1660             END;
1661
1662
1663
1664         xpaths := ARRAY[owning_lib, circ_lib, call_number, copy_number, status, location, circulate,
1665                         deposit, deposit_amount, ref, holdable, price, barcode, circ_modifier, circ_as_type,
1666                         alert_message, pub_note, priv_note, internal_id, stat_cat_data, parts_data, opac_visible];
1667
1668         FOR tmp_attr_set IN
1669                 SELECT  *
1670                   FROM  oils_xpath_tag_to_table( (SELECT marc FROM vandelay.queued_bib_record WHERE id = import_id), attr_def.tag, xpaths)
1671                             AS t( ol TEXT, clib TEXT, cn TEXT, cnum TEXT, cs TEXT, cl TEXT, circ TEXT,
1672                                   dep TEXT, dep_amount TEXT, r TEXT, hold TEXT, pr TEXT, bc TEXT, circ_mod TEXT,
1673                                   circ_as TEXT, amessage TEXT, note TEXT, pnote TEXT, internal_id TEXT,
1674                                   stat_cat_data TEXT, parts_data TEXT, opac_vis TEXT )
1675         LOOP
1676
1677             attr_set.import_error := NULL;
1678             attr_set.error_detail := NULL;
1679             attr_set.deposit_amount := NULL;
1680             attr_set.copy_number := NULL;
1681             attr_set.price := NULL;
1682             attr_set.circ_modifier := NULL;
1683             attr_set.location := NULL;
1684             attr_set.barcode := NULL;
1685             attr_set.call_number := NULL;
1686
1687             IF tmp_attr_set.pr != '' THEN
1688                 tmp_str = REGEXP_REPLACE(tmp_attr_set.pr, E'[^0-9\\.]', '', 'g');
1689                 IF tmp_str = '' THEN 
1690                     attr_set.import_error := 'import.item.invalid.price';
1691                     attr_set.error_detail := tmp_attr_set.pr; -- original value
1692                     RETURN NEXT attr_set; CONTINUE; 
1693                 END IF;
1694                 attr_set.price := tmp_str::NUMERIC(8,2); 
1695             END IF;
1696
1697             IF tmp_attr_set.dep_amount != '' THEN
1698                 tmp_str = REGEXP_REPLACE(tmp_attr_set.dep_amount, E'[^0-9\\.]', '', 'g');
1699                 IF tmp_str = '' THEN 
1700                     attr_set.import_error := 'import.item.invalid.deposit_amount';
1701                     attr_set.error_detail := tmp_attr_set.dep_amount; 
1702                     RETURN NEXT attr_set; CONTINUE; 
1703                 END IF;
1704                 attr_set.deposit_amount := tmp_str::NUMERIC(8,2); 
1705             END IF;
1706
1707             IF tmp_attr_set.cnum != '' THEN
1708                 tmp_str = REGEXP_REPLACE(tmp_attr_set.cnum, E'[^0-9]', '', 'g');
1709                 IF tmp_str = '' THEN 
1710                     attr_set.import_error := 'import.item.invalid.copy_number';
1711                     attr_set.error_detail := tmp_attr_set.cnum; 
1712                     RETURN NEXT attr_set; CONTINUE; 
1713                 END IF;
1714                 attr_set.copy_number := tmp_str::INT; 
1715             END IF;
1716
1717             IF tmp_attr_set.ol != '' THEN
1718                 SELECT id INTO attr_set.owning_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.ol); -- INT
1719                 IF NOT FOUND THEN
1720                     attr_set.import_error := 'import.item.invalid.owning_lib';
1721                     attr_set.error_detail := tmp_attr_set.ol;
1722                     RETURN NEXT attr_set; CONTINUE; 
1723                 END IF;
1724             END IF;
1725
1726             IF tmp_attr_set.clib != '' THEN
1727                 SELECT id INTO attr_set.circ_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.clib); -- INT
1728                 IF NOT FOUND THEN
1729                     attr_set.import_error := 'import.item.invalid.circ_lib';
1730                     attr_set.error_detail := tmp_attr_set.clib;
1731                     RETURN NEXT attr_set; CONTINUE; 
1732                 END IF;
1733             END IF;
1734
1735             IF tmp_attr_set.cs != '' THEN
1736                 SELECT id INTO attr_set.status FROM config.copy_status WHERE LOWER(name) = LOWER(tmp_attr_set.cs); -- INT
1737                 IF NOT FOUND THEN
1738                     attr_set.import_error := 'import.item.invalid.status';
1739                     attr_set.error_detail := tmp_attr_set.cs;
1740                     RETURN NEXT attr_set; CONTINUE; 
1741                 END IF;
1742             END IF;
1743
1744             IF COALESCE(tmp_attr_set.circ_mod, '') = '' THEN
1745
1746                 -- no circ mod defined, see if we should apply a default
1747                 SELECT INTO attr_set.circ_modifier TRIM(BOTH '"' FROM value) 
1748                     FROM actor.org_unit_ancestor_setting(
1749                         'vandelay.item.circ_modifier.default', 
1750                         attr_set.owning_lib
1751                     );
1752
1753                 -- make sure the value from the org setting is still valid
1754                 PERFORM 1 FROM config.circ_modifier WHERE code = attr_set.circ_modifier;
1755                 IF NOT FOUND THEN
1756                     attr_set.import_error := 'import.item.invalid.circ_modifier';
1757                     attr_set.error_detail := tmp_attr_set.circ_mod;
1758                     RETURN NEXT attr_set; CONTINUE; 
1759                 END IF;
1760
1761             ELSE 
1762
1763                 SELECT code INTO attr_set.circ_modifier FROM config.circ_modifier WHERE code = tmp_attr_set.circ_mod;
1764                 IF NOT FOUND THEN
1765                     attr_set.import_error := 'import.item.invalid.circ_modifier';
1766                     attr_set.error_detail := tmp_attr_set.circ_mod;
1767                     RETURN NEXT attr_set; CONTINUE; 
1768                 END IF;
1769             END IF;
1770
1771             IF tmp_attr_set.circ_as != '' THEN
1772                 SELECT code INTO attr_set.circ_as_type FROM config.coded_value_map WHERE ctype = 'item_type' AND code = tmp_attr_set.circ_as;
1773                 IF NOT FOUND THEN
1774                     attr_set.import_error := 'import.item.invalid.circ_as_type';
1775                     attr_set.error_detail := tmp_attr_set.circ_as;
1776                     RETURN NEXT attr_set; CONTINUE; 
1777                 END IF;
1778             END IF;
1779
1780             IF COALESCE(tmp_attr_set.cl, '') = '' THEN
1781                 -- no location specified, see if we should apply a default
1782
1783                 SELECT INTO attr_set.location TRIM(BOTH '"' FROM value) 
1784                     FROM actor.org_unit_ancestor_setting(
1785                         'vandelay.item.copy_location.default', 
1786                         attr_set.owning_lib
1787                     );
1788
1789                 -- make sure the value from the org setting is still valid
1790                 PERFORM 1 FROM asset.copy_location 
1791                     WHERE id = attr_set.location AND NOT deleted;
1792                 IF NOT FOUND THEN
1793                     attr_set.import_error := 'import.item.invalid.location';
1794                     attr_set.error_detail := tmp_attr_set.cs;
1795                     RETURN NEXT attr_set; CONTINUE; 
1796                 END IF;
1797             ELSE
1798
1799                 -- search up the org unit tree for a matching copy location
1800                 WITH RECURSIVE anscestor_depth AS (
1801                     SELECT  ou.id,
1802                         out.depth AS depth,
1803                         ou.parent_ou
1804                     FROM  actor.org_unit ou
1805                         JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
1806                     WHERE ou.id = COALESCE(attr_set.owning_lib, attr_set.circ_lib)
1807                         UNION ALL
1808                     SELECT  ou.id,
1809                         out.depth,
1810                         ou.parent_ou
1811                     FROM  actor.org_unit ou
1812                         JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
1813                         JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
1814                 ) SELECT  cpl.id INTO attr_set.location
1815                     FROM  anscestor_depth a
1816                         JOIN asset.copy_location cpl ON (cpl.owning_lib = a.id)
1817                     WHERE LOWER(cpl.name) = LOWER(tmp_attr_set.cl) 
1818                         AND NOT cpl.deleted
1819                     ORDER BY a.depth DESC
1820                     LIMIT 1; 
1821
1822                 IF NOT FOUND THEN
1823                     attr_set.import_error := 'import.item.invalid.location';
1824                     attr_set.error_detail := tmp_attr_set.cs;
1825                     RETURN NEXT attr_set; CONTINUE; 
1826                 END IF;
1827             END IF;
1828
1829             attr_set.circulate      :=
1830                 LOWER( SUBSTRING( tmp_attr_set.circ, 1, 1)) IN ('t','y','1')
1831                 OR LOWER(tmp_attr_set.circ) = 'circulating'; -- BOOL
1832
1833             attr_set.deposit        :=
1834                 LOWER( SUBSTRING( tmp_attr_set.dep, 1, 1 ) ) IN ('t','y','1')
1835                 OR LOWER(tmp_attr_set.dep) = 'deposit'; -- BOOL
1836
1837             attr_set.holdable       :=
1838                 LOWER( SUBSTRING( tmp_attr_set.hold, 1, 1 ) ) IN ('t','y','1')
1839                 OR LOWER(tmp_attr_set.hold) = 'holdable'; -- BOOL
1840
1841             attr_set.opac_visible   :=
1842                 LOWER( SUBSTRING( tmp_attr_set.opac_vis, 1, 1 ) ) IN ('t','y','1')
1843                 OR LOWER(tmp_attr_set.opac_vis) = 'visible'; -- BOOL
1844
1845             attr_set.ref            :=
1846                 LOWER( SUBSTRING( tmp_attr_set.r, 1, 1 ) ) IN ('t','y','1')
1847                 OR LOWER(tmp_attr_set.r) = 'reference'; -- BOOL
1848
1849             attr_set.call_number    := tmp_attr_set.cn; -- TEXT
1850             attr_set.barcode        := tmp_attr_set.bc; -- TEXT,
1851             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
1852             attr_set.pub_note       := tmp_attr_set.note; -- TEXT,
1853             attr_set.priv_note      := tmp_attr_set.pnote; -- TEXT,
1854             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
1855             attr_set.internal_id    := tmp_attr_set.internal_id::BIGINT;
1856             attr_set.stat_cat_data  := tmp_attr_set.stat_cat_data; -- TEXT,
1857             attr_set.parts_data     := tmp_attr_set.parts_data; -- TEXT,
1858
1859             RETURN NEXT attr_set;
1860
1861         END LOOP;
1862
1863     END IF;
1864
1865     RETURN;
1866
1867 END;
1868 $$ LANGUAGE PLPGSQL;
1869
1870
1871
1872 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_items ( ) RETURNS TRIGGER AS $func$
1873 DECLARE
1874     attr_def    BIGINT;
1875     item_data   vandelay.import_item%ROWTYPE;
1876 BEGIN
1877
1878     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1879         RETURN NEW;
1880     END IF;
1881
1882     SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
1883
1884     FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
1885         INSERT INTO vandelay.import_item (
1886             record,
1887             definition,
1888             owning_lib,
1889             circ_lib,
1890             call_number,
1891             copy_number,
1892             status,
1893             location,
1894             circulate,
1895             deposit,
1896             deposit_amount,
1897             ref,
1898             holdable,
1899             price,
1900             barcode,
1901             circ_modifier,
1902             circ_as_type,
1903             alert_message,
1904             pub_note,
1905             priv_note,
1906             internal_id,
1907             opac_visible,
1908             stat_cat_data,
1909             parts_data,
1910             import_error,
1911             error_detail
1912         ) VALUES (
1913             NEW.id,
1914             item_data.definition,
1915             item_data.owning_lib,
1916             item_data.circ_lib,
1917             item_data.call_number,
1918             item_data.copy_number,
1919             item_data.status,
1920             item_data.location,
1921             item_data.circulate,
1922             item_data.deposit,
1923             item_data.deposit_amount,
1924             item_data.ref,
1925             item_data.holdable,
1926             item_data.price,
1927             item_data.barcode,
1928             item_data.circ_modifier,
1929             item_data.circ_as_type,
1930             item_data.alert_message,
1931             item_data.pub_note,
1932             item_data.priv_note,
1933             item_data.internal_id,
1934             item_data.opac_visible,
1935             item_data.stat_cat_data,
1936             item_data.parts_data,
1937             item_data.import_error,
1938             item_data.error_detail
1939         );
1940     END LOOP;
1941
1942     RETURN NULL;
1943 END;
1944 $func$ LANGUAGE PLPGSQL;
1945
1946 CREATE TRIGGER ingest_item_trigger
1947     AFTER INSERT OR UPDATE ON vandelay.queued_bib_record
1948     FOR EACH ROW EXECUTE PROCEDURE vandelay.ingest_bib_items();
1949
1950
1951 -- evergreen.generic_map_normalizer 
1952
1953 CREATE OR REPLACE FUNCTION evergreen.generic_map_normalizer ( TEXT, TEXT ) RETURNS TEXT AS $f$
1954 my $string = shift;
1955 my %map;
1956
1957 my $default = $string;
1958
1959 $_ = shift;
1960 while (/^\s*?(.*?)\s*?=>\s*?(\S+)\s*/) {
1961     if ($1 eq '') {
1962         $default = $2;
1963     } else {
1964         $map{$2} = [split(/\s*,\s*/, $1)];
1965     }
1966     $_ = $';
1967 }
1968
1969 for my $key ( keys %map ) {
1970     return $key if (grep { $_ eq $string } @{ $map{$key} });
1971 }
1972
1973 return $default;
1974
1975 $f$ LANGUAGE PLPERLU;
1976
1977 CREATE OR REPLACE FUNCTION actor.address_alert_matches (
1978         org_unit INT, 
1979         street1 TEXT, 
1980         street2 TEXT, 
1981         city TEXT, 
1982         county TEXT, 
1983         state TEXT, 
1984         country TEXT, 
1985         post_code TEXT,
1986         mailing_address BOOL DEFAULT FALSE,
1987         billing_address BOOL DEFAULT FALSE
1988     ) RETURNS SETOF actor.address_alert AS $$
1989
1990 SELECT *
1991 FROM actor.address_alert
1992 WHERE
1993     active
1994     AND owner IN (SELECT id FROM actor.org_unit_ancestors($1)) 
1995     AND (
1996         (NOT mailing_address AND NOT billing_address)
1997         OR (mailing_address AND $9)
1998         OR (billing_address AND $10)
1999     )
2000     AND (
2001             (
2002                 match_all
2003                 AND COALESCE($2, '') ~* COALESCE(street1,   '.*')
2004                 AND COALESCE($3, '') ~* COALESCE(street2,   '.*')
2005                 AND COALESCE($4, '') ~* COALESCE(city,      '.*')
2006                 AND COALESCE($5, '') ~* COALESCE(county,    '.*')
2007                 AND COALESCE($6, '') ~* COALESCE(state,     '.*')
2008                 AND COALESCE($7, '') ~* COALESCE(country,   '.*')
2009                 AND COALESCE($8, '') ~* COALESCE(post_code, '.*')
2010             ) OR (
2011                 NOT match_all 
2012                 AND (  
2013                        $2 ~* street1
2014                     OR $3 ~* street2
2015                     OR $4 ~* city
2016                     OR $5 ~* county
2017                     OR $6 ~* state
2018                     OR $7 ~* country
2019                     OR $8 ~* post_code
2020                 )
2021             )
2022         )
2023     ORDER BY actor.org_unit_proximity(owner, $1)
2024 $$ LANGUAGE SQL;
2025
2026 CREATE OR REPLACE FUNCTION evergreen.coded_value_map_normalizer( input TEXT, ctype TEXT ) 
2027     RETURNS TEXT AS $F$
2028         SELECT COALESCE(value,$1) 
2029             FROM config.coded_value_map 
2030             WHERE ctype = $2 AND code = $1;
2031 $F$ LANGUAGE SQL;
2032
2033 -- user activity functions --
2034
2035 -- remove transient activity entries on insert of new entries
2036 CREATE OR REPLACE FUNCTION actor.usr_activity_transient_trg () RETURNS TRIGGER AS $$
2037 BEGIN
2038     DELETE FROM actor.usr_activity act USING config.usr_activity_type atype
2039         WHERE atype.transient AND 
2040             NEW.etype = atype.id AND
2041             act.etype = atype.id AND
2042             act.usr = NEW.usr;
2043     RETURN NEW;
2044 END;
2045 $$ LANGUAGE PLPGSQL;
2046
2047 CREATE TRIGGER remove_transient_usr_activity
2048     BEFORE INSERT ON actor.usr_activity
2049     FOR EACH ROW EXECUTE PROCEDURE actor.usr_activity_transient_trg();
2050
2051 -- given a set of activity criteria, find the most approprate activity type
2052 CREATE OR REPLACE FUNCTION actor.usr_activity_get_type (
2053         ewho TEXT, 
2054         ewhat TEXT, 
2055         ehow TEXT
2056     ) RETURNS SETOF config.usr_activity_type AS $$
2057 SELECT * FROM config.usr_activity_type 
2058     WHERE 
2059         enabled AND 
2060         (ewho  IS NULL OR ewho  = $1) AND
2061         (ewhat IS NULL OR ewhat = $2) AND
2062         (ehow  IS NULL OR ehow  = $3) 
2063     ORDER BY 
2064         -- BOOL comparisons sort false to true
2065         COALESCE(ewho, '')  != COALESCE($1, ''),
2066         COALESCE(ewhat,'')  != COALESCE($2, ''),
2067         COALESCE(ehow, '')  != COALESCE($3, '') 
2068     LIMIT 1;
2069 $$ LANGUAGE SQL;
2070
2071 -- given a set of activity criteria, finds the best
2072 -- activity type and inserts the activity entry
2073 CREATE OR REPLACE FUNCTION actor.insert_usr_activity (
2074         usr INT,
2075         ewho TEXT, 
2076         ewhat TEXT, 
2077         ehow TEXT
2078     ) RETURNS SETOF actor.usr_activity AS $$
2079 DECLARE
2080     new_row actor.usr_activity%ROWTYPE;
2081 BEGIN
2082     SELECT id INTO new_row.etype FROM actor.usr_activity_get_type(ewho, ewhat, ehow);
2083     IF FOUND THEN
2084         new_row.usr := usr;
2085         INSERT INTO actor.usr_activity (usr, etype) 
2086             VALUES (usr, new_row.etype)
2087             RETURNING * INTO new_row;
2088         RETURN NEXT new_row;
2089     END IF;
2090 END;
2091 $$ LANGUAGE plpgsql;
2092
2093 CREATE OR REPLACE FUNCTION evergreen.rel_bump(terms TEXT[], value TEXT, bumps TEXT[], mults NUMERIC[]) RETURNS NUMERIC AS
2094 $BODY$
2095 use strict;
2096 my ($terms,$value,$bumps,$mults) = @_;
2097
2098 my $retval = 1;
2099
2100 for (my $id = 0; $id < @$bumps; $id++) {
2101         if ($bumps->[$id] eq 'first_word') {
2102                 $retval *= $mults->[$id] if ($value =~ /^$terms->[0]/);
2103         } elsif ($bumps->[$id] eq 'full_match') {
2104                 my $fullmatch = join(' ', @$terms);
2105                 $retval *= $mults->[$id] if ($value =~ /^$fullmatch$/);
2106         } elsif ($bumps->[$id] eq 'word_order') {
2107                 my $wordorder = join('.*', @$terms);
2108                 $retval *= $mults->[$id] if ($value =~ /$wordorder/);
2109         }
2110 }
2111 return $retval;
2112 $BODY$ LANGUAGE plperlu IMMUTABLE STRICT COST 100;
2113
2114 -- user activity functions --
2115
2116
2117 -- find the most relevant set of credentials for the Z source and org
2118 CREATE OR REPLACE FUNCTION config.z3950_source_credentials_lookup
2119         (source TEXT, owner INTEGER) 
2120         RETURNS config.z3950_source_credentials AS $$
2121
2122     SELECT creds.* 
2123     FROM config.z3950_source_credentials creds
2124         JOIN actor.org_unit aou ON (aou.id = creds.owner)
2125         JOIN actor.org_unit_type aout ON (aout.id = aou.ou_type)
2126     WHERE creds.source = $1 AND creds.owner IN ( 
2127         SELECT id FROM actor.org_unit_ancestors($2) 
2128     )
2129     ORDER BY aout.depth DESC LIMIT 1;
2130
2131 $$ LANGUAGE SQL STABLE;
2132
2133 -- since we are not exposing config.z3950_source_credentials
2134 -- via the IDL, providing a stored proc gives us a way to
2135 -- set values in the table via cstore
2136 CREATE OR REPLACE FUNCTION config.z3950_source_credentials_apply
2137         (src TEXT, org INTEGER, uname TEXT, passwd TEXT) 
2138         RETURNS VOID AS $$
2139 BEGIN
2140     PERFORM 1 FROM config.z3950_source_credentials
2141         WHERE owner = org AND source = src;
2142
2143     IF FOUND THEN
2144         IF COALESCE(uname, '') = '' AND COALESCE(passwd, '') = '' THEN
2145             DELETE FROM config.z3950_source_credentials 
2146                 WHERE owner = org AND source = src;
2147         ELSE 
2148             UPDATE config.z3950_source_credentials 
2149                 SET username = uname, password = passwd
2150                 WHERE owner = org AND source = src;
2151         END IF;
2152     ELSE
2153         IF COALESCE(uname, '') <> '' OR COALESCE(passwd, '') <> '' THEN
2154             INSERT INTO config.z3950_source_credentials
2155                 (source, owner, username, password) 
2156                 VALUES (src, org, uname, passwd);
2157         END IF;
2158     END IF;
2159 END;
2160 $$ LANGUAGE PLPGSQL;
2161
2162 -- Handy function for transforming marc to a variant available on config.xml_transform
2163 CREATE OR REPLACE FUNCTION evergreen.marc_to (marc text, xfrm text) RETURNS TEXT AS $$
2164     SELECT evergreen.xml_pretty_print(xslt_process($1,xslt)::XML)::TEXT FROM config.xml_transform WHERE name = $2;
2165 $$ LANGUAGE SQL;
2166