805fbe4807e048368b627a4f6bb3b3e9e195b8e1
[migration-tools.git] / sql / base / 07-eg-specific.sql
1
2 CREATE OR REPLACE FUNCTION migration_tools.create_staff_user(
3     username TEXT,
4     password TEXT,
5     org TEXT,
6     perm_group TEXT,
7     first_name TEXT DEFAULT '',
8     last_name TEXT DEFAULT ''
9 ) RETURNS VOID AS $func$
10 BEGIN
11     RAISE NOTICE '%', org ;
12     INSERT INTO actor.usr (usrname, passwd, ident_type, first_given_name, family_name, home_ou, profile)
13     SELECT username, password, 1, first_name, last_name, aou.id, pgt.id
14     FROM   actor.org_unit aou, permission.grp_tree pgt
15     WHERE  aou.shortname = org
16     AND    pgt.name = perm_group;
17 END
18 $func$
19 LANGUAGE PLPGSQL;
20
21 -- FIXME: testing for STAFF_LOGIN perm is probably better
22 CREATE OR REPLACE FUNCTION migration_tools.is_staff_profile (INT) RETURNS BOOLEAN AS $$
23   DECLARE
24     profile ALIAS FOR $1;
25   BEGIN
26     RETURN CASE WHEN 'Staff' IN (select (permission.grp_ancestors(profile)).name) THEN TRUE ELSE FALSE END;
27   END;
28 $$ LANGUAGE PLPGSQL STRICT STABLE;
29
30 -- TODO: make another version of the procedure below that can work with specified copy staging tables
31 -- The following should track the logic of OpenILS::Application::AppUtils::get_copy_price
32 CREATE OR REPLACE FUNCTION migration_tools.get_copy_price( item BIGINT ) RETURNS NUMERIC AS $$
33 DECLARE
34     context_lib             INT;
35     charge_lost_on_zero     BOOLEAN;
36     min_price               NUMERIC;
37     max_price               NUMERIC;
38     default_price           NUMERIC;
39     working_price           NUMERIC;
40
41 BEGIN
42
43     SELECT INTO context_lib CASE WHEN call_number = -1 THEN circ_lib ELSE owning_lib END
44         FROM asset.copy ac, asset.call_number acn WHERE ac.call_number = acn.id AND ac.id = item;
45
46     SELECT INTO charge_lost_on_zero value
47         FROM actor.org_unit_ancestor_setting('circ.charge_lost_on_zero',context_lib);
48
49     SELECT INTO min_price value
50         FROM actor.org_unit_ancestor_setting('circ.min_item_price',context_lib);
51
52     SELECT INTO max_price value
53         FROM actor.org_unit_ancestor_setting('circ.max_item_price',context_lib);
54
55     SELECT INTO default_price value
56         FROM actor.org_unit_ancestor_setting('cat.default_item_price',context_lib);
57
58     SELECT INTO working_price price FROM asset.copy WHERE id = item;
59
60     IF (working_price IS NULL OR (working_price = 0 AND charge_lost_on_zero)) THEN
61         working_price := default_price;
62     END IF;
63
64     IF (max_price IS NOT NULL AND working_price > max_price) THEN
65         working_price := max_price;
66     END IF;
67
68     IF (min_price IS NOT NULL AND working_price < min_price) THEN
69         IF (working_price <> 0 OR charge_lost_on_zero IS NULL OR charge_lost_on_zero) THEN
70             working_price := min_price;
71         END IF;
72     END IF;
73
74     RETURN working_price;
75
76 END;
77
78 $$ LANGUAGE plpgsql;
79
80 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix( tablename TEXT ) RETURNS VOID AS $$
81
82 -- Usage:
83 --
84 --   First make sure the circ matrix is loaded and the circulations
85 --   have been staged to the extent possible (but at the very least
86 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
87 --   circ modifiers must also be in place.
88 --
89 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
90 --
91
92 DECLARE
93   circ_lib             INT;
94   target_copy          INT;
95   usr                  INT;
96   is_renewal           BOOLEAN;
97   this_duration_rule   INT;
98   this_fine_rule       INT;
99   this_max_fine_rule   INT;
100   rcd                  config.rule_circ_duration%ROWTYPE;
101   rrf                  config.rule_recurring_fine%ROWTYPE;
102   rmf                  config.rule_max_fine%ROWTYPE;
103   circ                 INT;
104   n                    INT := 0;
105   n_circs              INT;
106   
107 BEGIN
108
109   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
110
111   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
112
113     -- Fetch the correct rules for this circulation
114     EXECUTE ('
115       SELECT
116         circ_lib,
117         target_copy,
118         usr,
119         CASE
120           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
121           ELSE FALSE
122         END
123       FROM ' || tablename || ' WHERE id = ' || circ || ';')
124       INTO circ_lib, target_copy, usr, is_renewal ;
125     SELECT
126       INTO this_duration_rule,
127            this_fine_rule,
128            this_max_fine_rule
129       duration_rule,
130       recurring_fine_rule,
131       max_fine_rule
132       FROM action.item_user_circ_test(
133         circ_lib,
134         target_copy,
135         usr,
136         is_renewal
137         );
138     SELECT INTO rcd * FROM config.rule_circ_duration
139       WHERE id = this_duration_rule;
140     SELECT INTO rrf * FROM config.rule_recurring_fine
141       WHERE id = this_fine_rule;
142     SELECT INTO rmf * FROM config.rule_max_fine
143       WHERE id = this_max_fine_rule;
144
145     -- Apply the rules to this circulation
146     EXECUTE ('UPDATE ' || tablename || ' c
147     SET
148       duration_rule = rcd.name,
149       recurring_fine_rule = rrf.name,
150       max_fine_rule = rmf.name,
151       duration = rcd.normal,
152       recurring_fine = rrf.normal,
153       max_fine =
154         CASE rmf.is_percent
155           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
156           ELSE rmf.amount
157         END,
158       renewal_remaining = rcd.max_renewals
159     FROM
160       config.rule_circ_duration rcd,
161       config.rule_recurring_fine rrf,
162       config.rule_max_fine rmf,
163                         asset.copy ac
164     WHERE
165       rcd.id = ' || this_duration_rule || ' AND
166       rrf.id = ' || this_fine_rule || ' AND
167       rmf.id = ' || this_max_fine_rule || ' AND
168                         ac.id = c.target_copy AND
169       c.id = ' || circ || ';');
170
171     -- Keep track of where we are in the process
172     n := n + 1;
173     IF (n % 100 = 0) THEN
174       RAISE INFO '%', n || ' of ' || n_circs
175         || ' (' || (100*n/n_circs) || '%) circs updated.';
176     END IF;
177
178   END LOOP;
179
180   RETURN;
181 END;
182
183 $$ LANGUAGE plpgsql;
184
185 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix_to_specific_circ( tablename TEXT, circ BIGINT ) RETURNS VOID AS $$
186
187 -- Usage:
188 --
189 --   First make sure the circ matrix is loaded and the circulations
190 --   have been staged to the extent possible (but at the very least
191 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
192 --   circ modifiers must also be in place.
193 --
194 --   SELECT migration_tools.apply_circ_matrix_to_specific_circ('m_nwrl.action_circulation', 18391960);
195 --
196
197 DECLARE
198   circ_lib             INT;
199   target_copy          INT;
200   usr                  INT;
201   is_renewal           BOOLEAN;
202   this_duration_rule   INT;
203   this_fine_rule       INT;
204   this_max_fine_rule   INT;
205   rcd                  config.rule_circ_duration%ROWTYPE;
206   rrf                  config.rule_recurring_fine%ROWTYPE;
207   rmf                  config.rule_max_fine%ROWTYPE;
208   n                    INT := 0;
209   n_circs              INT := 1;
210   
211 BEGIN
212
213   --EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
214
215   --FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
216
217     -- Fetch the correct rules for this circulation
218     EXECUTE ('
219       SELECT
220         circ_lib,
221         target_copy,
222         usr,
223         CASE
224           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
225           ELSE FALSE
226         END
227       FROM ' || tablename || ' WHERE id = ' || circ || ';')
228       INTO circ_lib, target_copy, usr, is_renewal ;
229     SELECT
230       INTO this_duration_rule,
231            this_fine_rule,
232            this_max_fine_rule
233       (matchpoint).duration_rule,
234       (matchpoint).recurring_fine_rule,
235       (matchpoint).max_fine_rule
236       FROM action.find_circ_matrix_matchpoint(
237         circ_lib,
238         target_copy,
239         usr,
240         is_renewal
241         );
242     SELECT INTO rcd * FROM config.rule_circ_duration
243       WHERE id = this_duration_rule;
244     SELECT INTO rrf * FROM config.rule_recurring_fine
245       WHERE id = this_fine_rule;
246     SELECT INTO rmf * FROM config.rule_max_fine
247       WHERE id = this_max_fine_rule;
248
249     -- Apply the rules to this circulation
250     EXECUTE ('UPDATE ' || tablename || ' c
251     SET
252       duration_rule = rcd.name,
253       recurring_fine_rule = rrf.name,
254       max_fine_rule = rmf.name,
255       duration = rcd.normal,
256       recurring_fine = rrf.normal,
257       max_fine =
258         CASE rmf.is_percent
259           WHEN TRUE THEN (rmf.amount / 100.0) * migration_tools.get_copy_price(ac.id)
260           ELSE rmf.amount
261         END,
262       renewal_remaining = rcd.max_renewals,
263       grace_period = rrf.grace_period
264     FROM
265       config.rule_circ_duration rcd,
266       config.rule_recurring_fine rrf,
267       config.rule_max_fine rmf,
268                         asset.copy ac
269     WHERE
270       rcd.id = ' || this_duration_rule || ' AND
271       rrf.id = ' || this_fine_rule || ' AND
272       rmf.id = ' || this_max_fine_rule || ' AND
273                         ac.id = c.target_copy AND
274       c.id = ' || circ || ';');
275
276     -- Keep track of where we are in the process
277     n := n + 1;
278     IF (n % 100 = 0) THEN
279       RAISE INFO '%', n || ' of ' || n_circs
280         || ' (' || (100*n/n_circs) || '%) circs updated.';
281     END IF;
282
283   --END LOOP;
284
285   RETURN;
286 END;
287
288 $$ LANGUAGE plpgsql;
289
290 CREATE OR REPLACE FUNCTION migration_tools.assign_standing_penalties ( ) RETURNS VOID AS $$
291
292 -- USAGE: Once circulation data has been loaded, and group penalty thresholds have been set up, run this.
293 --        This will assign standing penalties as needed.
294
295 DECLARE
296   org_unit  INT;
297   usr       INT;
298
299 BEGIN
300
301   FOR org_unit IN EXECUTE ('SELECT DISTINCT org_unit FROM permission.grp_penalty_threshold;') LOOP
302
303     FOR usr IN EXECUTE ('SELECT id FROM actor.usr WHERE NOT deleted;') LOOP
304   
305       EXECUTE('SELECT actor.calculate_system_penalties(' || usr || ', ' || org_unit || ');');
306
307     END LOOP;
308
309   END LOOP;
310
311   RETURN;
312
313 END;
314
315 $$ LANGUAGE plpgsql;
316
317 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_pristine_database () RETURNS VOID AS $$
318
319 BEGIN
320   INSERT INTO metabib.metarecord (fingerprint, master_record)
321     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
322       FROM  biblio.record_entry b
323       WHERE NOT b.deleted
324         AND b.id IN (SELECT r.id FROM biblio.record_entry r LEFT JOIN metabib.metarecord_source_map k ON (k.source = r.id) WHERE k.id IS NULL AND r.fingerprint IS NOT NULL)
325         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
326       ORDER BY b.fingerprint, b.quality DESC;
327   INSERT INTO metabib.metarecord_source_map (metarecord, source)
328     SELECT  m.id, r.id
329       FROM  biblio.record_entry r
330       JOIN  metabib.metarecord m USING (fingerprint)
331      WHERE  NOT r.deleted;
332 END;
333   
334 $$ LANGUAGE plpgsql;
335
336
337 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_incumbent_database () RETURNS VOID AS $$
338
339 BEGIN
340   INSERT INTO metabib.metarecord (fingerprint, master_record)
341     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
342       FROM  biblio.record_entry b
343       WHERE NOT b.deleted
344         AND b.id IN (SELECT r.id FROM biblio.record_entry r LEFT JOIN metabib.metarecord_source_map k ON (k.source = r.id) WHERE k.id IS NULL AND r.fingerprint IS NOT NULL)
345         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
346       ORDER BY b.fingerprint, b.quality DESC;
347   INSERT INTO metabib.metarecord_source_map (metarecord, source)
348     SELECT  m.id, r.id
349       FROM  biblio.record_entry r
350         JOIN metabib.metarecord m USING (fingerprint)
351       WHERE NOT r.deleted
352         AND r.id IN (SELECT b.id FROM biblio.record_entry b LEFT JOIN metabib.metarecord_source_map k ON (k.source = b.id) WHERE k.id IS NULL);
353 END;
354     
355 $$ LANGUAGE plpgsql;
356
357 CREATE OR REPLACE FUNCTION migration_tools.create_cards( schemaname TEXT ) RETURNS VOID AS $$
358
359 -- USAGE: Make sure the patrons are staged in schemaname.actor_usr_legacy and have 'usrname' assigned.
360 --        Then SELECT migration_tools.create_cards('m_foo');
361
362 DECLARE
363         u                    TEXT := schemaname || '.actor_usr_legacy';
364         c                    TEXT := schemaname || '.actor_card';
365   
366 BEGIN
367
368         EXECUTE ('DELETE FROM ' || c || ';');
369         EXECUTE ('INSERT INTO ' || c || ' (usr, barcode) SELECT id, usrname FROM ' || u || ';');
370         EXECUTE ('UPDATE ' || u || ' u SET card = c.id FROM ' || c || ' c WHERE c.usr = u.id;');
371
372   RETURN;
373
374 END;
375
376 $$ LANGUAGE plpgsql;
377
378 CREATE OR REPLACE FUNCTION migration_tools.check_ou_depth ( ) RETURNS VOID AS $$
379
380 DECLARE
381   ou  INT;
382         org_unit_depth INT;
383         ou_parent INT;
384         parent_depth INT;
385   errors_found BOOLEAN;
386         ou_shortname TEXT;
387         parent_shortname TEXT;
388         ou_type_name TEXT;
389         parent_type TEXT;
390         type_id INT;
391         type_depth INT;
392         type_parent INT;
393         type_parent_depth INT;
394         proper_parent TEXT;
395
396 BEGIN
397
398         errors_found := FALSE;
399
400 -- Checking actor.org_unit_type
401
402         FOR type_id IN EXECUTE ('SELECT id FROM actor.org_unit_type ORDER BY id;') LOOP
403
404                 SELECT depth FROM actor.org_unit_type WHERE id = type_id INTO type_depth;
405                 SELECT parent FROM actor.org_unit_type WHERE id = type_id INTO type_parent;
406
407                 IF type_parent IS NOT NULL THEN
408
409                         SELECT depth FROM actor.org_unit_type WHERE id = type_parent INTO type_parent_depth;
410
411                         IF type_depth - type_parent_depth <> 1 THEN
412                                 SELECT name FROM actor.org_unit_type WHERE id = type_id INTO ou_type_name;
413                                 SELECT name FROM actor.org_unit_type WHERE id = type_parent INTO parent_type;
414                                 RAISE INFO 'The % org unit type has a depth of %, but its parent org unit type, %, has a depth of %.',
415                                         ou_type_name, type_depth, parent_type, type_parent_depth;
416                                 errors_found := TRUE;
417
418                         END IF;
419
420                 END IF;
421
422         END LOOP;
423
424 -- Checking actor.org_unit
425
426   FOR ou IN EXECUTE ('SELECT id FROM actor.org_unit ORDER BY shortname;') LOOP
427
428                 SELECT parent_ou FROM actor.org_unit WHERE id = ou INTO ou_parent;
429                 SELECT t.depth FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou INTO org_unit_depth;
430                 SELECT t.depth FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou_parent INTO parent_depth;
431                 SELECT shortname FROM actor.org_unit WHERE id = ou INTO ou_shortname;
432                 SELECT shortname FROM actor.org_unit WHERE id = ou_parent INTO parent_shortname;
433                 SELECT t.name FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou INTO ou_type_name;
434                 SELECT t.name FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou_parent INTO parent_type;
435
436                 IF ou_parent IS NOT NULL THEN
437
438                         IF      (org_unit_depth - parent_depth <> 1) OR (
439                                 (SELECT parent FROM actor.org_unit_type WHERE name = ou_type_name) <> (SELECT id FROM actor.org_unit_type WHERE name = parent_type)
440                         ) THEN
441                                 RAISE INFO '% (org unit %) is a % (depth %) but its parent, % (org unit %), is a % (depth %).', 
442                                         ou_shortname, ou, ou_type_name, org_unit_depth, parent_shortname, ou_parent, parent_type, parent_depth;
443                                 errors_found := TRUE;
444                         END IF;
445
446                 END IF;
447
448   END LOOP;
449
450         IF NOT errors_found THEN
451                 RAISE INFO 'No errors found.';
452         END IF;
453
454   RETURN;
455
456 END;
457
458 $$ LANGUAGE plpgsql;
459
460 CREATE OR REPLACE FUNCTION migration_tools.simple_export_library_config(dir TEXT, orgs INT[]) RETURNS VOID AS $FUNC$
461 BEGIN
462    EXECUTE $$COPY (SELECT * FROM actor.hours_of_operation WHERE id IN ($$ ||
463            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
464            $$) TO '$$ ||  dir || $$/actor_hours_of_operation'$$;
465    EXECUTE $$COPY (SELECT org_unit, close_start, close_end, reason FROM actor.org_unit_closed WHERE org_unit IN ($$ ||
466            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
467            $$) TO '$$ ||  dir || $$/actor_org_unit_closed'$$;
468    EXECUTE $$COPY (SELECT org_unit, name, value FROM actor.org_unit_setting WHERE org_unit IN ($$ ||
469            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
470            $$) TO '$$ ||  dir || $$/actor_org_unit_setting'$$;
471    EXECUTE $$COPY (SELECT name, owning_lib, holdable, hold_verify, opac_visible, circulate FROM asset.copy_location WHERE owning_lib IN ($$ ||
472            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
473            $$) TO '$$ ||  dir || $$/asset_copy_location'$$;
474    EXECUTE $$COPY (SELECT grp, org_unit, penalty, threshold FROM permission.grp_penalty_threshold WHERE org_unit IN ($$ ||
475            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
476            $$) TO '$$ ||  dir || $$/permission_grp_penalty_threshold'$$;
477    EXECUTE $$COPY (SELECT owning_lib, label, label_sortkey FROM asset.call_number_prefix WHERE owning_lib IN ($$ ||
478            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
479            $$) TO '$$ ||  dir || $$/asset_call_number_prefix'$$;
480    EXECUTE $$COPY (SELECT owning_lib, label, label_sortkey FROM asset.call_number_suffix WHERE owning_lib IN ($$ ||
481            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
482            $$) TO '$$ ||  dir || $$/asset_call_number_suffix'$$;
483    EXECUTE $$COPY config.rule_circ_duration TO '$$ ||  dir || $$/config_rule_circ_duration'$$;
484    EXECUTE $$COPY config.rule_age_hold_protect TO '$$ ||  dir || $$/config_rule_age_hold_protect'$$;
485    EXECUTE $$COPY config.rule_max_fine TO '$$ ||  dir || $$/config_rule_max_fine'$$;
486    EXECUTE $$COPY config.rule_recurring_fine TO '$$ ||  dir || $$/config_rule_recurring_fine'$$;
487    EXECUTE $$COPY permission.grp_tree TO '$$ ||  dir || $$/permission_grp_tree'$$;
488 END;
489 $FUNC$ LANGUAGE PLPGSQL;
490
491 CREATE OR REPLACE FUNCTION migration_tools.simple_import_library_config(dir TEXT) RETURNS VOID AS $FUNC$
492 BEGIN
493    EXECUTE $$COPY actor.hours_of_operation FROM '$$ ||  dir || $$/actor_hours_of_operation'$$;
494    EXECUTE $$COPY actor.org_unit_closed (org_unit, close_start, close_end, reason) FROM '$$ ||  dir || $$/actor_org_unit_closed'$$;
495    EXECUTE $$COPY actor.org_unit_setting (org_unit, name, value) FROM '$$ ||  dir || $$/actor_org_unit_setting'$$;
496    EXECUTE $$COPY asset.copy_location (name, owning_lib, holdable, hold_verify, opac_visible, circulate) FROM '$$ ||  dir || $$/asset_copy_location'$$;
497    EXECUTE $$COPY permission.grp_penalty_threshold (grp, org_unit, penalty, threshold) FROM '$$ ||  dir || $$/permission_grp_penalty_threshold'$$;
498    EXECUTE $$COPY asset.call_number_prefix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/asset_call_number_prefix'$$;
499    EXECUTE $$COPY asset.call_number_suffix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/asset_call_number_suffix'$$;
500
501    -- import any new circ rules
502    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_circ_duration', 'id', 'name');
503    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_age_hold_protect', 'id', 'name');
504    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_max_fine', 'id', 'name');
505    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_recurring_fine', 'id', 'name');
506
507    -- and permission groups
508    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'permission', 'grp_tree', 'id', 'name');
509
510 END;
511 $FUNC$ LANGUAGE PLPGSQL;
512
513 -- example: SELECT * FROM migration_tools.duplicate_template(5,'{3,4}');
514 CREATE OR REPLACE FUNCTION migration_tools.duplicate_template (INTEGER, INTEGER[]) RETURNS VOID AS $$
515     DECLARE
516         target_event_def ALIAS FOR $1;
517         orgs ALIAS FOR $2;
518     BEGIN
519         DROP TABLE IF EXISTS new_atevdefs;
520         CREATE TEMP TABLE new_atevdefs (atevdef INTEGER);
521         FOR i IN array_lower(orgs,1) .. array_upper(orgs,1) LOOP
522             INSERT INTO action_trigger.event_definition (
523                 active
524                 ,owner
525                 ,name
526                 ,hook
527                 ,validator
528                 ,reactor
529                 ,cleanup_success
530                 ,cleanup_failure
531                 ,delay
532                 ,max_delay
533                 ,usr_field
534                 ,opt_in_setting
535                 ,delay_field
536                 ,group_field
537                 ,template
538                 ,granularity
539                 ,repeat_delay
540             ) SELECT
541                 'f'
542                 ,orgs[i]
543                 ,name || ' (clone of '||target_event_def||')'
544                 ,hook
545                 ,validator
546                 ,reactor
547                 ,cleanup_success
548                 ,cleanup_failure
549                 ,delay
550                 ,max_delay
551                 ,usr_field
552                 ,opt_in_setting
553                 ,delay_field
554                 ,group_field
555                 ,template
556                 ,granularity
557                 ,repeat_delay
558             FROM
559                 action_trigger.event_definition
560             WHERE
561                 id = target_event_def
562             ;
563             RAISE INFO 'created atevdef with id = %', currval('action_trigger.event_definition_id_seq');
564             INSERT INTO new_atevdefs SELECT currval('action_trigger.event_definition_id_seq');
565             INSERT INTO action_trigger.environment (
566                 event_def
567                 ,path
568                 ,collector
569                 ,label
570             ) SELECT
571                 currval('action_trigger.event_definition_id_seq')
572                 ,path
573                 ,collector
574                 ,label
575             FROM
576                 action_trigger.environment
577             WHERE
578                 event_def = target_event_def
579             ;
580             INSERT INTO action_trigger.event_params (
581                 event_def
582                 ,param
583                 ,value
584             ) SELECT
585                 currval('action_trigger.event_definition_id_seq')
586                 ,param
587                 ,value
588             FROM
589                 action_trigger.event_params
590             WHERE
591                 event_def = target_event_def
592             ;
593         END LOOP;
594         RAISE INFO '-- UPDATE action_trigger.event_definition SET active = CASE WHEN id = % THEN FALSE ELSE TRUE END WHERE id in (%,%);', target_event_def, target_event_def, (SELECT array_to_string(array_agg(atevdef),',') from new_atevdefs);
595     END;
596 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
597
598 -- example: SELECT * FROM migration_tools.duplicate_template_but_change_delay(5,'{3,4}','00:30:00'::INTERVAL);
599 CREATE OR REPLACE FUNCTION migration_tools.duplicate_template_but_change_delay (INTEGER, INTEGER[], INTERVAL) RETURNS VOID AS $$
600     DECLARE
601         target_event_def ALIAS FOR $1;
602         orgs ALIAS FOR $2;
603         new_interval ALIAS FOR $3;
604     BEGIN
605         DROP TABLE IF EXISTS new_atevdefs;
606         CREATE TEMP TABLE new_atevdefs (atevdef INTEGER);
607         FOR i IN array_lower(orgs,1) .. array_upper(orgs,1) LOOP
608             INSERT INTO action_trigger.event_definition (
609                 active
610                 ,owner
611                 ,name
612                 ,hook
613                 ,validator
614                 ,reactor
615                 ,cleanup_success
616                 ,cleanup_failure
617                 ,delay
618                 ,max_delay
619                 ,usr_field
620                 ,opt_in_setting
621                 ,delay_field
622                 ,group_field
623                 ,template
624                 ,granularity
625                 ,repeat_delay
626             ) SELECT
627                 'f'
628                 ,orgs[i]
629                 ,name || ' (clone of '||target_event_def||')'
630                 ,hook
631                 ,validator
632                 ,reactor
633                 ,cleanup_success
634                 ,cleanup_failure
635                 ,new_interval
636                 ,max_delay
637                 ,usr_field
638                 ,opt_in_setting
639                 ,delay_field
640                 ,group_field
641                 ,template
642                 ,granularity
643                 ,repeat_delay
644             FROM
645                 action_trigger.event_definition
646             WHERE
647                 id = target_event_def
648             ;
649             RAISE INFO 'created atevdef with id = %', currval('action_trigger.event_definition_id_seq');
650             INSERT INTO new_atevdefs SELECT currval('action_trigger.event_definition_id_seq');
651             INSERT INTO action_trigger.environment (
652                 event_def
653                 ,path
654                 ,collector
655                 ,label
656             ) SELECT
657                 currval('action_trigger.event_definition_id_seq')
658                 ,path
659                 ,collector
660                 ,label
661             FROM
662                 action_trigger.environment
663             WHERE
664                 event_def = target_event_def
665             ;
666             INSERT INTO action_trigger.event_params (
667                 event_def
668                 ,param
669                 ,value
670             ) SELECT
671                 currval('action_trigger.event_definition_id_seq')
672                 ,param
673                 ,value
674             FROM
675                 action_trigger.event_params
676             WHERE
677                 event_def = target_event_def
678             ;
679         END LOOP;
680         RAISE INFO '-- UPDATE action_trigger.event_definition SET active = CASE WHEN id = % THEN FALSE ELSE TRUE END WHERE id in (%,%);', target_event_def, target_event_def, (SELECT array_to_string(array_agg(atevdef),',') from new_atevdefs);
681     END;
682 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
683
684 -- example: SELECT * FROM migration_tools.duplicate_templates(3,'{5,6}');
685 CREATE OR REPLACE FUNCTION migration_tools.duplicate_templates (INTEGER, INTEGER[]) RETURNS VOID AS $$
686     DECLARE
687         org ALIAS FOR $1;
688         target_event_defs ALIAS FOR $2;
689     BEGIN
690         DROP TABLE IF EXISTS new_atevdefs;
691         CREATE TEMP TABLE new_atevdefs (atevdef INTEGER);
692         FOR i IN array_lower(target_event_defs,1) .. array_upper(target_event_defs,1) LOOP
693             INSERT INTO action_trigger.event_definition (
694                 active
695                 ,owner
696                 ,name
697                 ,hook
698                 ,validator
699                 ,reactor
700                 ,cleanup_success
701                 ,cleanup_failure
702                 ,delay
703                 ,max_delay
704                 ,usr_field
705                 ,opt_in_setting
706                 ,delay_field
707                 ,group_field
708                 ,template
709                 ,granularity
710                 ,repeat_delay
711             ) SELECT
712                 'f'
713                 ,org
714                 ,name || ' (clone of '||target_event_defs[i]||')'
715                 ,hook
716                 ,validator
717                 ,reactor
718                 ,cleanup_success
719                 ,cleanup_failure
720                 ,delay
721                 ,max_delay
722                 ,usr_field
723                 ,opt_in_setting
724                 ,delay_field
725                 ,group_field
726                 ,template
727                 ,granularity
728                 ,repeat_delay
729             FROM
730                 action_trigger.event_definition
731             WHERE
732                 id = target_event_defs[i]
733             ;
734             RAISE INFO 'created atevdef with id = %', currval('action_trigger.event_definition_id_seq');
735             INSERT INTO new_atevdefs SELECT currval('action_trigger.event_definition_id_seq');
736             INSERT INTO action_trigger.environment (
737                 event_def
738                 ,path
739                 ,collector
740                 ,label
741             ) SELECT
742                 currval('action_trigger.event_definition_id_seq')
743                 ,path
744                 ,collector
745                 ,label
746             FROM
747                 action_trigger.environment
748             WHERE
749                 event_def = target_event_defs[i]
750             ;
751             INSERT INTO action_trigger.event_params (
752                 event_def
753                 ,param
754                 ,value
755             ) SELECT
756                 currval('action_trigger.event_definition_id_seq')
757                 ,param
758                 ,value
759             FROM
760                 action_trigger.event_params
761             WHERE
762                 event_def = target_event_defs[i]
763             ;
764         END LOOP;
765         RAISE INFO '-- UPDATE action_trigger.event_definition SET active = TRUE WHERE id in (%);', (SELECT array_to_string(array_agg(atevdef),',') from new_atevdefs);
766     END;
767 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
768
769 CREATE OR REPLACE FUNCTION migration_tools.reset_event (BIGINT) RETURNS VOID AS $$
770     UPDATE
771         action_trigger.event
772     SET
773          start_time = NULL
774         ,update_time = NULL
775         ,complete_time = NULL
776         ,update_process = NULL
777         ,state = 'pending'
778         ,template_output = NULL
779         ,error_output = NULL
780         ,async_output = NULL
781     WHERE
782         id = $1;
783 $$ LANGUAGE SQL;
784
785 CREATE OR REPLACE FUNCTION migration_tools.find_hold_matrix_matchpoint (INTEGER) RETURNS INTEGER AS $$
786     SELECT action.find_hold_matrix_matchpoint(
787         (SELECT pickup_lib FROM action.hold_request WHERE id = $1),
788         (SELECT request_lib FROM action.hold_request WHERE id = $1),
789         (SELECT current_copy FROM action.hold_request WHERE id = $1),
790         (SELECT usr FROM action.hold_request WHERE id = $1),
791         (SELECT requestor FROM action.hold_request WHERE id = $1)
792     );
793 $$ LANGUAGE SQL;
794
795 CREATE OR REPLACE FUNCTION migration_tools.find_hold_matrix_matchpoint2 (INTEGER) RETURNS SETOF action.matrix_test_result AS $$
796     SELECT action.hold_request_permit_test(
797         (SELECT pickup_lib FROM action.hold_request WHERE id = $1),
798         (SELECT request_lib FROM action.hold_request WHERE id = $1),
799         (SELECT current_copy FROM action.hold_request WHERE id = $1),
800         (SELECT usr FROM action.hold_request WHERE id = $1),
801         (SELECT requestor FROM action.hold_request WHERE id = $1)
802     );
803 $$ LANGUAGE SQL;
804
805 CREATE OR REPLACE FUNCTION migration_tools.find_circ_matrix_matchpoint (INTEGER) RETURNS SETOF action.found_circ_matrix_matchpoint AS $$
806     SELECT action.find_circ_matrix_matchpoint(
807         (SELECT circ_lib FROM action.circulation WHERE id = $1),
808         (SELECT target_copy FROM action.circulation WHERE id = $1),
809         (SELECT usr FROM action.circulation WHERE id = $1),
810         (SELECT COALESCE(
811                 NULLIF(phone_renewal,false),
812                 NULLIF(desk_renewal,false),
813                 NULLIF(opac_renewal,false),
814                 false
815             ) FROM action.circulation WHERE id = $1
816         )
817     );
818 $$ LANGUAGE SQL;
819
820 -- set a new salted password
821
822 CREATE OR REPLACE FUNCTION migration_tools.set_salted_passwd(INTEGER,TEXT) RETURNS BOOLEAN AS $$
823     DECLARE
824         usr_id              ALIAS FOR $1;
825         plain_passwd        ALIAS FOR $2;
826         plain_salt          TEXT;
827         md5_passwd          TEXT;
828     BEGIN
829
830         SELECT actor.create_salt('main') INTO plain_salt;
831
832         SELECT MD5(plain_passwd) INTO md5_passwd;
833         
834         PERFORM actor.set_passwd(usr_id, 'main', MD5(plain_salt || md5_passwd), plain_salt);
835
836         RETURN TRUE;
837
838     END;
839 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
840
841 -- internal function for handle_shelf
842
843 CREATE OR REPLACE FUNCTION migration_tools._handle_shelf (TEXT,TEXT,TEXT,INTEGER,BOOLEAN) RETURNS VOID AS $$
844     DECLARE
845         table_schema ALIAS FOR $1;
846         table_name ALIAS FOR $2;
847         org_shortname ALIAS FOR $3;
848         org_range ALIAS FOR $4;
849         make_assertion ALIAS FOR $5;
850         proceed BOOLEAN;
851         org INTEGER;
852         -- if x_org is on the mapping table, it'll take precedence over the passed org_shortname param
853         -- though we'll still use the passed org for the full path traversal when needed
854         x_org_found BOOLEAN;
855         x_org INTEGER;
856         org_list INTEGER[];
857         o INTEGER;
858         row_count NUMERIC;
859     BEGIN
860         EXECUTE 'SELECT EXISTS (
861             SELECT 1
862             FROM information_schema.columns
863             WHERE table_schema = $1
864             AND table_name = $2
865             and column_name = ''desired_shelf''
866         )' INTO proceed USING table_schema, table_name;
867         IF NOT proceed THEN
868             RAISE EXCEPTION 'Missing column desired_shelf';
869         END IF;
870
871         EXECUTE 'SELECT EXISTS (
872             SELECT 1
873             FROM information_schema.columns
874             WHERE table_schema = $1
875             AND table_name = $2
876             and column_name = ''x_org''
877         )' INTO x_org_found USING table_schema, table_name;
878
879         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
880         IF org IS NULL THEN
881             RAISE EXCEPTION 'Cannot find org by shortname';
882         END IF;
883
884         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
885
886         EXECUTE 'ALTER TABLE '
887             || quote_ident(table_name)
888             || ' DROP COLUMN IF EXISTS x_shelf';
889         EXECUTE 'ALTER TABLE '
890             || quote_ident(table_name)
891             || ' ADD COLUMN x_shelf INTEGER';
892
893         IF x_org_found THEN
894             RAISE INFO 'Found x_org column';
895             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
896                 || ' SET x_shelf = b.id FROM asset_copy_location b'
897                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
898                 || ' AND b.owning_lib = x_org'
899                 || ' AND NOT b.deleted';
900             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
901                 || ' SET x_shelf = b.id FROM asset.copy_location b'
902                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
903                 || ' AND b.owning_lib = x_org'
904                 || ' AND x_shelf IS NULL'
905                 || ' AND NOT b.deleted';
906         ELSE
907             RAISE INFO 'Did not find x_org column';
908             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
909                 || ' SET x_shelf = b.id FROM asset_copy_location b'
910                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
911                 || ' AND b.owning_lib = $1'
912                 || ' AND NOT b.deleted'
913             USING org;
914             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
915                 || ' SET x_shelf = b.id FROM asset_copy_location b'
916                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
917                 || ' AND b.owning_lib = $1'
918                 || ' AND x_shelf IS NULL'
919                 || ' AND NOT b.deleted'
920             USING org;
921         END IF;
922
923         FOREACH o IN ARRAY org_list LOOP
924             RAISE INFO 'Considering org %', o;
925             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
926                 || ' SET x_shelf = b.id FROM asset.copy_location b'
927                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
928                 || ' AND b.owning_lib = $1 AND x_shelf IS NULL'
929                 || ' AND NOT b.deleted'
930             USING o;
931             GET DIAGNOSTICS row_count = ROW_COUNT;
932             RAISE INFO 'Updated % rows', row_count;
933         END LOOP;
934
935         IF make_assertion THEN
936             EXECUTE 'SELECT migration_tools.assert(
937                 NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_shelf <> '''' AND x_shelf IS NULL),
938                 ''Cannot find a desired location'',
939                 ''Found all desired locations''
940             );';
941         END IF;
942
943     END;
944 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
945
946 -- convenience functions for handling copy_location maps
947 CREATE OR REPLACE FUNCTION migration_tools.handle_shelf (TEXT,TEXT,TEXT,INTEGER) RETURNS VOID AS $$
948     SELECT migration_tools._handle_shelf($1,$2,$3,$4,TRUE);
949 $$ LANGUAGE SQL;
950
951 -- convenience functions for handling circmod maps
952
953 CREATE OR REPLACE FUNCTION migration_tools.handle_circmod (TEXT,TEXT) RETURNS VOID AS $$
954     DECLARE
955         table_schema ALIAS FOR $1;
956         table_name ALIAS FOR $2;
957         proceed BOOLEAN;
958     BEGIN
959         EXECUTE 'SELECT EXISTS (
960             SELECT 1
961             FROM information_schema.columns
962             WHERE table_schema = $1
963             AND table_name = $2
964             and column_name = ''desired_circmod''
965         )' INTO proceed USING table_schema, table_name;
966         IF NOT proceed THEN
967             RAISE EXCEPTION 'Missing column desired_circmod'; 
968         END IF;
969
970         EXECUTE 'ALTER TABLE '
971             || quote_ident(table_name)
972             || ' DROP COLUMN IF EXISTS x_circmod';
973         EXECUTE 'ALTER TABLE '
974             || quote_ident(table_name)
975             || ' ADD COLUMN x_circmod TEXT';
976
977         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
978             || ' SET x_circmod = code FROM config.circ_modifier b'
979             || ' WHERE BTRIM(UPPER(a.desired_circmod)) = BTRIM(UPPER(b.code))';
980
981         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
982             || ' SET x_circmod = code FROM config.circ_modifier b'
983             || ' WHERE BTRIM(UPPER(a.desired_circmod)) = BTRIM(UPPER(b.name))'
984             || ' AND x_circmod IS NULL';
985
986         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
987             || ' SET x_circmod = code FROM config.circ_modifier b'
988             || ' WHERE BTRIM(UPPER(a.desired_circmod)) = BTRIM(UPPER(b.description))'
989             || ' AND x_circmod IS NULL';
990
991         EXECUTE 'SELECT migration_tools.assert(
992             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_circmod <> '''' AND x_circmod IS NULL),
993             ''Cannot find a desired circulation modifier'',
994             ''Found all desired circulation modifiers''
995         );';
996
997     END;
998 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
999
1000 -- convenience functions for handling item status maps
1001
1002 CREATE OR REPLACE FUNCTION migration_tools.handle_status (TEXT,TEXT) RETURNS VOID AS $$
1003     DECLARE
1004         table_schema ALIAS FOR $1;
1005         table_name ALIAS FOR $2;
1006         proceed BOOLEAN;
1007     BEGIN
1008         EXECUTE 'SELECT EXISTS (
1009             SELECT 1
1010             FROM information_schema.columns
1011             WHERE table_schema = $1
1012             AND table_name = $2
1013             and column_name = ''desired_status''
1014         )' INTO proceed USING table_schema, table_name;
1015         IF NOT proceed THEN
1016             RAISE EXCEPTION 'Missing column desired_status'; 
1017         END IF;
1018
1019         EXECUTE 'ALTER TABLE '
1020             || quote_ident(table_name)
1021             || ' DROP COLUMN IF EXISTS x_status';
1022         EXECUTE 'ALTER TABLE '
1023             || quote_ident(table_name)
1024             || ' ADD COLUMN x_status INTEGER';
1025
1026         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1027             || ' SET x_status = id FROM config.copy_status b'
1028             || ' WHERE BTRIM(UPPER(a.desired_status)) = BTRIM(UPPER(b.name))';
1029
1030         EXECUTE 'SELECT migration_tools.assert(
1031             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_status <> '''' AND x_status IS NULL),
1032             ''Cannot find a desired copy status'',
1033             ''Found all desired copy statuses''
1034         );';
1035
1036     END;
1037 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1038
1039 -- convenience functions for handling org maps
1040
1041 CREATE OR REPLACE FUNCTION migration_tools.handle_org (TEXT,TEXT) RETURNS VOID AS $$
1042     DECLARE
1043         table_schema ALIAS FOR $1;
1044         table_name ALIAS FOR $2;
1045         proceed BOOLEAN;
1046     BEGIN
1047         EXECUTE 'SELECT EXISTS (
1048             SELECT 1
1049             FROM information_schema.columns
1050             WHERE table_schema = $1
1051             AND table_name = $2
1052             and column_name = ''desired_org''
1053         )' INTO proceed USING table_schema, table_name;
1054         IF NOT proceed THEN
1055             RAISE EXCEPTION 'Missing column desired_org'; 
1056         END IF;
1057
1058         EXECUTE 'ALTER TABLE '
1059             || quote_ident(table_name)
1060             || ' DROP COLUMN IF EXISTS x_org';
1061         EXECUTE 'ALTER TABLE '
1062             || quote_ident(table_name)
1063             || ' ADD COLUMN x_org INTEGER';
1064
1065         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1066             || ' SET x_org = b.id FROM actor.org_unit b'
1067             || ' WHERE BTRIM(a.desired_org) = BTRIM(b.shortname)';
1068
1069         EXECUTE 'SELECT migration_tools.assert(
1070             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_org <> '''' AND x_org IS NULL),
1071             ''Cannot find a desired org unit'',
1072             ''Found all desired org units''
1073         );';
1074
1075     END;
1076 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1077
1078 -- convenience function for handling desired_not_migrate
1079
1080 CREATE OR REPLACE FUNCTION migration_tools.handle_not_migrate (TEXT,TEXT) RETURNS VOID AS $$
1081     DECLARE
1082         table_schema ALIAS FOR $1;
1083         table_name ALIAS FOR $2;
1084         proceed BOOLEAN;
1085     BEGIN
1086         EXECUTE 'SELECT EXISTS (
1087             SELECT 1
1088             FROM information_schema.columns
1089             WHERE table_schema = $1
1090             AND table_name = $2
1091             and column_name = ''desired_not_migrate''
1092         )' INTO proceed USING table_schema, table_name;
1093         IF NOT proceed THEN
1094             RAISE EXCEPTION 'Missing column desired_not_migrate'; 
1095         END IF;
1096
1097         EXECUTE 'ALTER TABLE '
1098             || quote_ident(table_name)
1099             || ' DROP COLUMN IF EXISTS x_migrate';
1100         EXECUTE 'ALTER TABLE '
1101             || quote_ident(table_name)
1102             || ' ADD COLUMN x_migrate BOOLEAN';
1103
1104         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1105             || ' SET x_migrate = CASE'
1106             || ' WHEN BTRIM(desired_not_migrate) = ''TRUE'' THEN FALSE'
1107             || ' WHEN BTRIM(desired_not_migrate) = ''DNM'' THEN FALSE'
1108             || ' WHEN BTRIM(desired_not_migrate) = ''Do Not Migrate'' THEN FALSE'
1109             || ' WHEN BTRIM(desired_not_migrate) = ''FALSE'' THEN TRUE'
1110             || ' WHEN BTRIM(desired_not_migrate) = ''Migrate'' THEN TRUE'
1111             || ' WHEN BTRIM(desired_not_migrate) = '''' THEN TRUE'
1112             || ' END';
1113
1114         EXECUTE 'SELECT migration_tools.assert(
1115             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE x_migrate IS NULL),
1116             ''Not all desired_not_migrate values understood'',
1117             ''All desired_not_migrate values understood''
1118         );';
1119
1120     END;
1121 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1122
1123 -- convenience function for handling desired_not_migrate
1124
1125 CREATE OR REPLACE FUNCTION migration_tools.handle_barred_or_blocked (TEXT,TEXT) RETURNS VOID AS $$
1126     DECLARE
1127         table_schema ALIAS FOR $1;
1128         table_name ALIAS FOR $2;
1129         proceed BOOLEAN;
1130     BEGIN
1131         EXECUTE 'SELECT EXISTS (
1132             SELECT 1
1133             FROM information_schema.columns
1134             WHERE table_schema = $1
1135             AND table_name = $2
1136             and column_name = ''desired_barred_or_blocked''
1137         )' INTO proceed USING table_schema, table_name;
1138         IF NOT proceed THEN
1139             RAISE EXCEPTION 'Missing column desired_barred_or_blocked'; 
1140         END IF;
1141
1142         EXECUTE 'ALTER TABLE '
1143             || quote_ident(table_name)
1144             || ' DROP COLUMN IF EXISTS x_barred';
1145         EXECUTE 'ALTER TABLE '
1146             || quote_ident(table_name)
1147             || ' ADD COLUMN x_barred BOOLEAN';
1148
1149         EXECUTE 'ALTER TABLE '
1150             || quote_ident(table_name)
1151             || ' DROP COLUMN IF EXISTS x_blocked';
1152         EXECUTE 'ALTER TABLE '
1153             || quote_ident(table_name)
1154             || ' ADD COLUMN x_blocked BOOLEAN';
1155
1156         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1157             || ' SET x_barred = CASE'
1158             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Barred'' THEN TRUE'
1159             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Blocked'' THEN FALSE'
1160             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Neither'' THEN FALSE'
1161             || ' WHEN BTRIM(desired_barred_or_blocked) = '''' THEN FALSE'
1162             || ' END';
1163
1164         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1165             || ' SET x_blocked = CASE'
1166             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Blocked'' THEN TRUE'
1167             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Barred'' THEN FALSE'
1168             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Neither'' THEN FALSE'
1169             || ' WHEN BTRIM(desired_barred_or_blocked) = '''' THEN FALSE'
1170             || ' END';
1171
1172         EXECUTE 'SELECT migration_tools.assert(
1173             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE x_barred IS NULL or x_blocked IS NULL),
1174             ''Not all desired_barred_or_blocked values understood'',
1175             ''All desired_barred_or_blocked values understood''
1176         );';
1177
1178     END;
1179 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1180
1181 -- convenience function for handling desired_profile
1182
1183 CREATE OR REPLACE FUNCTION migration_tools.handle_profile (TEXT,TEXT) RETURNS VOID AS $$
1184     DECLARE
1185         table_schema ALIAS FOR $1;
1186         table_name ALIAS FOR $2;
1187         proceed BOOLEAN;
1188     BEGIN
1189         EXECUTE 'SELECT EXISTS (
1190             SELECT 1
1191             FROM information_schema.columns
1192             WHERE table_schema = $1
1193             AND table_name = $2
1194             and column_name = ''desired_profile''
1195         )' INTO proceed USING table_schema, table_name;
1196         IF NOT proceed THEN
1197             RAISE EXCEPTION 'Missing column desired_profile'; 
1198         END IF;
1199
1200         EXECUTE 'ALTER TABLE '
1201             || quote_ident(table_name)
1202             || ' DROP COLUMN IF EXISTS x_profile';
1203         EXECUTE 'ALTER TABLE '
1204             || quote_ident(table_name)
1205             || ' ADD COLUMN x_profile INTEGER';
1206
1207         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1208             || ' SET x_profile = b.id FROM permission.grp_tree b'
1209             || ' WHERE BTRIM(UPPER(a.desired_profile)) = BTRIM(UPPER(b.name))';
1210
1211         EXECUTE 'SELECT migration_tools.assert(
1212             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_profile <> '''' AND x_profile IS NULL),
1213             ''Cannot find a desired profile'',
1214             ''Found all desired profiles''
1215         );';
1216
1217     END;
1218 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1219
1220 -- convenience function for handling desired actor stat cats
1221
1222 CREATE OR REPLACE FUNCTION migration_tools.vivicate_actor_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1223     DECLARE
1224         table_schema ALIAS FOR $1;
1225         table_name ALIAS FOR $2;
1226         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1227         org_shortname ALIAS FOR $4;
1228         proceed BOOLEAN;
1229         org INTEGER;
1230         org_list INTEGER[];
1231         sc TEXT;
1232         sce TEXT;
1233     BEGIN
1234
1235         SELECT 'desired_sc' || field_suffix INTO sc;
1236         SELECT 'desired_sce' || field_suffix INTO sce;
1237
1238         EXECUTE 'SELECT EXISTS (
1239             SELECT 1
1240             FROM information_schema.columns
1241             WHERE table_schema = $1
1242             AND table_name = $2
1243             and column_name = $3
1244         )' INTO proceed USING table_schema, table_name, sc;
1245         IF NOT proceed THEN
1246             RAISE EXCEPTION 'Missing column %', sc; 
1247         END IF;
1248         EXECUTE 'SELECT EXISTS (
1249             SELECT 1
1250             FROM information_schema.columns
1251             WHERE table_schema = $1
1252             AND table_name = $2
1253             and column_name = $3
1254         )' INTO proceed USING table_schema, table_name, sce;
1255         IF NOT proceed THEN
1256             RAISE EXCEPTION 'Missing column %', sce; 
1257         END IF;
1258
1259         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1260         IF org IS NULL THEN
1261             RAISE EXCEPTION 'Cannot find org by shortname';
1262         END IF;
1263         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1264
1265         -- caller responsible for their own truncates though we try to prevent duplicates
1266         EXECUTE 'INSERT INTO actor_stat_cat (owner, name)
1267             SELECT DISTINCT
1268                  $1
1269                 ,BTRIM('||sc||')
1270             FROM 
1271                 ' || quote_ident(table_name) || '
1272             WHERE
1273                 NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1274                 AND NOT EXISTS (
1275                     SELECT id
1276                     FROM actor.stat_cat
1277                     WHERE owner = ANY ($2)
1278                     AND name = BTRIM('||sc||')
1279                 )
1280                 AND NOT EXISTS (
1281                     SELECT id
1282                     FROM actor_stat_cat
1283                     WHERE owner = ANY ($2)
1284                     AND name = BTRIM('||sc||')
1285                 )
1286             ORDER BY 2;'
1287         USING org, org_list;
1288
1289         EXECUTE 'INSERT INTO actor_stat_cat_entry (stat_cat, owner, value)
1290             SELECT DISTINCT
1291                 COALESCE(
1292                     (SELECT id
1293                         FROM actor.stat_cat
1294                         WHERE owner = ANY ($2)
1295                         AND BTRIM('||sc||') = BTRIM(name))
1296                    ,(SELECT id
1297                         FROM actor_stat_cat
1298                         WHERE owner = ANY ($2)
1299                         AND BTRIM('||sc||') = BTRIM(name))
1300                 )
1301                 ,$1
1302                 ,BTRIM('||sce||')
1303             FROM 
1304                 ' || quote_ident(table_name) || '
1305             WHERE
1306                     NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1307                 AND NULLIF(BTRIM('||sce||'),'''') IS NOT NULL
1308                 AND NOT EXISTS (
1309                     SELECT id
1310                     FROM actor.stat_cat_entry
1311                     WHERE stat_cat = (
1312                         SELECT id
1313                         FROM actor.stat_cat
1314                         WHERE owner = ANY ($2)
1315                         AND BTRIM('||sc||') = BTRIM(name)
1316                     ) AND value = BTRIM('||sce||')
1317                     AND owner = ANY ($2)
1318                 )
1319                 AND NOT EXISTS (
1320                     SELECT id
1321                     FROM actor_stat_cat_entry
1322                     WHERE stat_cat = (
1323                         SELECT id
1324                         FROM actor_stat_cat
1325                         WHERE owner = ANY ($2)
1326                         AND BTRIM('||sc||') = BTRIM(name)
1327                     ) AND value = BTRIM('||sce||')
1328                     AND owner = ANY ($2)
1329                 )
1330             ORDER BY 1,3;'
1331         USING org, org_list;
1332     END;
1333 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1334
1335 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1336     DECLARE
1337         table_schema ALIAS FOR $1;
1338         table_name ALIAS FOR $2;
1339         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1340         org_shortname ALIAS FOR $4;
1341         proceed BOOLEAN;
1342         org INTEGER;
1343         org_list INTEGER[];
1344         o INTEGER;
1345         sc TEXT;
1346         sce TEXT;
1347     BEGIN
1348         SELECT 'desired_sc' || field_suffix INTO sc;
1349         SELECT 'desired_sce' || field_suffix INTO sce;
1350         EXECUTE 'SELECT EXISTS (
1351             SELECT 1
1352             FROM information_schema.columns
1353             WHERE table_schema = $1
1354             AND table_name = $2
1355             and column_name = $3
1356         )' INTO proceed USING table_schema, table_name, sc;
1357         IF NOT proceed THEN
1358             RAISE EXCEPTION 'Missing column %', sc; 
1359         END IF;
1360         EXECUTE 'SELECT EXISTS (
1361             SELECT 1
1362             FROM information_schema.columns
1363             WHERE table_schema = $1
1364             AND table_name = $2
1365             and column_name = $3
1366         )' INTO proceed USING table_schema, table_name, sce;
1367         IF NOT proceed THEN
1368             RAISE EXCEPTION 'Missing column %', sce; 
1369         END IF;
1370
1371         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1372         IF org IS NULL THEN
1373             RAISE EXCEPTION 'Cannot find org by shortname';
1374         END IF;
1375
1376         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1377
1378         EXECUTE 'ALTER TABLE '
1379             || quote_ident(table_name)
1380             || ' DROP COLUMN IF EXISTS x_sc' || field_suffix;
1381         EXECUTE 'ALTER TABLE '
1382             || quote_ident(table_name)
1383             || ' ADD COLUMN x_sc' || field_suffix || ' INTEGER';
1384         EXECUTE 'ALTER TABLE '
1385             || quote_ident(table_name)
1386             || ' DROP COLUMN IF EXISTS x_sce' || field_suffix;
1387         EXECUTE 'ALTER TABLE '
1388             || quote_ident(table_name)
1389             || ' ADD COLUMN x_sce' || field_suffix || ' INTEGER';
1390
1391
1392         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1393             SET
1394                 x_sc' || field_suffix || ' = id
1395             FROM
1396                 (SELECT id, name, owner FROM actor_stat_cat
1397                     UNION SELECT id, name, owner FROM actor.stat_cat) u
1398             WHERE
1399                     BTRIM(UPPER(u.name)) = BTRIM(UPPER(' || sc || '))
1400                 AND u.owner = ANY ($1);'
1401         USING org_list;
1402
1403         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1404             SET
1405                 x_sce' || field_suffix || ' = id
1406             FROM
1407                 (SELECT id, stat_cat, owner, value FROM actor_stat_cat_entry
1408                     UNION SELECT id, stat_cat, owner, value FROM actor.stat_cat_entry) u
1409             WHERE
1410                     u.stat_cat = x_sc' || field_suffix || '
1411                 AND BTRIM(UPPER(u.value)) = BTRIM(UPPER(' || sce || '))
1412                 AND u.owner = ANY ($1);'
1413         USING org_list;
1414
1415         EXECUTE 'SELECT migration_tools.assert(
1416             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sc' || field_suffix || ' <> '''' AND x_sc' || field_suffix || ' IS NULL),
1417             ''Cannot find a desired stat cat'',
1418             ''Found all desired stat cats''
1419         );';
1420
1421         EXECUTE 'SELECT migration_tools.assert(
1422             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sce' || field_suffix || ' <> '''' AND x_sce' || field_suffix || ' IS NULL),
1423             ''Cannot find a desired stat cat entry'',
1424             ''Found all desired stat cat entries''
1425         );';
1426
1427     END;
1428 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1429
1430 -- convenience functions for adding shelving locations
1431 DROP FUNCTION IF EXISTS migration_tools.find_shelf(INT,TEXT);
1432 CREATE OR REPLACE FUNCTION migration_tools.find_shelf(org_id INT, shelf_name TEXT) RETURNS INTEGER AS $$
1433 DECLARE
1434     return_id   INT;
1435     d           INT;
1436     cur_id      INT;
1437 BEGIN
1438     SELECT INTO d MAX(distance) FROM actor.org_unit_ancestors_distance(org_id);
1439     WHILE d >= 0
1440     LOOP
1441         SELECT INTO cur_id id FROM actor.org_unit_ancestor_at_depth(org_id,d);
1442         SELECT INTO return_id id FROM asset.copy_location WHERE owning_lib = cur_id AND name ILIKE shelf_name;
1443         IF return_id IS NOT NULL THEN
1444                 RETURN return_id;
1445         END IF;
1446         d := d - 1;
1447     END LOOP;
1448
1449     RETURN NULL;
1450 END
1451 $$ LANGUAGE plpgsql;
1452
1453 -- may remove later but testing using this with new migration scripts and not loading acls until go live
1454
1455 DROP FUNCTION IF EXISTS migration_tools.find_mig_shelf(INT,TEXT);
1456 CREATE OR REPLACE FUNCTION migration_tools.find_mig_shelf(org_id INT, shelf_name TEXT) RETURNS INTEGER AS $$
1457 DECLARE
1458     return_id   INT;
1459     d           INT;
1460     cur_id      INT;
1461 BEGIN
1462     SELECT INTO d MAX(distance) FROM actor.org_unit_ancestors_distance(org_id);
1463     WHILE d >= 0
1464     LOOP
1465         SELECT INTO cur_id id FROM actor.org_unit_ancestor_at_depth(org_id,d);
1466         
1467         SELECT INTO return_id id FROM 
1468             (SELECT * FROM asset.copy_location UNION ALL SELECT * FROM asset_copy_location) x
1469             WHERE owning_lib = cur_id AND name ILIKE shelf_name;
1470         IF return_id IS NOT NULL THEN
1471                 RETURN return_id;
1472         END IF;
1473         d := d - 1;
1474     END LOOP;
1475
1476     RETURN NULL;
1477 END
1478 $$ LANGUAGE plpgsql;
1479
1480 -- convenience function for linking to the item staging table
1481
1482 CREATE OR REPLACE FUNCTION migration_tools.handle_item_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
1483     DECLARE
1484         table_schema ALIAS FOR $1;
1485         table_name ALIAS FOR $2;
1486         foreign_column_name ALIAS FOR $3;
1487         main_column_name ALIAS FOR $4;
1488         btrim_desired ALIAS FOR $5;
1489         proceed BOOLEAN;
1490     BEGIN
1491         EXECUTE 'SELECT EXISTS (
1492             SELECT 1
1493             FROM information_schema.columns
1494             WHERE table_schema = $1
1495             AND table_name = $2
1496             and column_name = $3
1497         )' INTO proceed USING table_schema, table_name, foreign_column_name;
1498         IF NOT proceed THEN
1499             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
1500         END IF;
1501
1502         EXECUTE 'SELECT EXISTS (
1503             SELECT 1
1504             FROM information_schema.columns
1505             WHERE table_schema = $1
1506             AND table_name = ''asset_copy_legacy''
1507             and column_name = $2
1508         )' INTO proceed USING table_schema, main_column_name;
1509         IF NOT proceed THEN
1510             RAISE EXCEPTION 'No %.asset_copy_legacy with column %', table_schema, main_column_name; 
1511         END IF;
1512
1513         EXECUTE 'ALTER TABLE '
1514             || quote_ident(table_name)
1515             || ' DROP COLUMN IF EXISTS x_item';
1516         EXECUTE 'ALTER TABLE '
1517             || quote_ident(table_name)
1518             || ' ADD COLUMN x_item BIGINT';
1519
1520         IF btrim_desired THEN
1521             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1522                 || ' SET x_item = b.id FROM asset_copy_legacy b'
1523                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
1524                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
1525         ELSE
1526             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1527                 || ' SET x_item = b.id FROM asset_copy_legacy b'
1528                 || ' WHERE a.' || quote_ident(foreign_column_name)
1529                 || ' = b.' || quote_ident(main_column_name);
1530         END IF;
1531
1532         --EXECUTE 'SELECT migration_tools.assert(
1533         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_item IS NULL),
1534         --    ''Cannot link every barcode'',
1535         --    ''Every barcode linked''
1536         --);';
1537
1538     END;
1539 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1540
1541 -- convenience function for linking to the user staging table
1542
1543 CREATE OR REPLACE FUNCTION migration_tools.handle_user_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
1544     DECLARE
1545         table_schema ALIAS FOR $1;
1546         table_name ALIAS FOR $2;
1547         foreign_column_name ALIAS FOR $3;
1548         main_column_name ALIAS FOR $4;
1549         btrim_desired ALIAS FOR $5;
1550         proceed BOOLEAN;
1551     BEGIN
1552         EXECUTE 'SELECT EXISTS (
1553             SELECT 1
1554             FROM information_schema.columns
1555             WHERE table_schema = $1
1556             AND table_name = $2
1557             and column_name = $3
1558         )' INTO proceed USING table_schema, table_name, foreign_column_name;
1559         IF NOT proceed THEN
1560             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
1561         END IF;
1562
1563         EXECUTE 'SELECT EXISTS (
1564             SELECT 1
1565             FROM information_schema.columns
1566             WHERE table_schema = $1
1567             AND table_name = ''actor_usr_legacy''
1568             and column_name = $2
1569         )' INTO proceed USING table_schema, main_column_name;
1570         IF NOT proceed THEN
1571             RAISE EXCEPTION 'No %.actor_usr_legacy with column %', table_schema, main_column_name; 
1572         END IF;
1573
1574         EXECUTE 'ALTER TABLE '
1575             || quote_ident(table_name)
1576             || ' DROP COLUMN IF EXISTS x_user';
1577         EXECUTE 'ALTER TABLE '
1578             || quote_ident(table_name)
1579             || ' ADD COLUMN x_user INTEGER';
1580
1581         IF btrim_desired THEN
1582             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1583                 || ' SET x_user = b.id FROM actor_usr_legacy b'
1584                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
1585                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
1586         ELSE
1587             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1588                 || ' SET x_user = b.id FROM actor_usr_legacy b'
1589                 || ' WHERE a.' || quote_ident(foreign_column_name)
1590                 || ' = b.' || quote_ident(main_column_name);
1591         END IF;
1592
1593         --EXECUTE 'SELECT migration_tools.assert(
1594         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_user IS NULL),
1595         --    ''Cannot link every barcode'',
1596         --    ''Every barcode linked''
1597         --);';
1598
1599     END;
1600 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1601
1602 -- convenience function for handling desired asset stat cats
1603
1604 CREATE OR REPLACE FUNCTION migration_tools.vivicate_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1605     DECLARE
1606         table_schema ALIAS FOR $1;
1607         table_name ALIAS FOR $2;
1608         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1609         org_shortname ALIAS FOR $4;
1610         proceed BOOLEAN;
1611         org INTEGER;
1612         org_list INTEGER[];
1613         sc TEXT;
1614         sce TEXT;
1615     BEGIN
1616
1617         SELECT 'desired_sc' || field_suffix INTO sc;
1618         SELECT 'desired_sce' || field_suffix INTO sce;
1619
1620         EXECUTE 'SELECT EXISTS (
1621             SELECT 1
1622             FROM information_schema.columns
1623             WHERE table_schema = $1
1624             AND table_name = $2
1625             and column_name = $3
1626         )' INTO proceed USING table_schema, table_name, sc;
1627         IF NOT proceed THEN
1628             RAISE EXCEPTION 'Missing column %', sc; 
1629         END IF;
1630         EXECUTE 'SELECT EXISTS (
1631             SELECT 1
1632             FROM information_schema.columns
1633             WHERE table_schema = $1
1634             AND table_name = $2
1635             and column_name = $3
1636         )' INTO proceed USING table_schema, table_name, sce;
1637         IF NOT proceed THEN
1638             RAISE EXCEPTION 'Missing column %', sce; 
1639         END IF;
1640
1641         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1642         IF org IS NULL THEN
1643             RAISE EXCEPTION 'Cannot find org by shortname';
1644         END IF;
1645         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1646
1647         -- caller responsible for their own truncates though we try to prevent duplicates
1648         EXECUTE 'INSERT INTO asset_stat_cat (owner, name)
1649             SELECT DISTINCT
1650                  $1
1651                 ,BTRIM('||sc||')
1652             FROM 
1653                 ' || quote_ident(table_name) || '
1654             WHERE
1655                 NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1656                 AND NOT EXISTS (
1657                     SELECT id
1658                     FROM asset.stat_cat
1659                     WHERE owner = ANY ($2)
1660                     AND name = BTRIM('||sc||')
1661                 )
1662                 AND NOT EXISTS (
1663                     SELECT id
1664                     FROM asset_stat_cat
1665                     WHERE owner = ANY ($2)
1666                     AND name = BTRIM('||sc||')
1667                 )
1668             ORDER BY 2;'
1669         USING org, org_list;
1670
1671         EXECUTE 'INSERT INTO asset_stat_cat_entry (stat_cat, owner, value)
1672             SELECT DISTINCT
1673                 COALESCE(
1674                     (SELECT id
1675                         FROM asset.stat_cat
1676                         WHERE owner = ANY ($2)
1677                         AND BTRIM('||sc||') = BTRIM(name))
1678                    ,(SELECT id
1679                         FROM asset_stat_cat
1680                         WHERE owner = ANY ($2)
1681                         AND BTRIM('||sc||') = BTRIM(name))
1682                 )
1683                 ,$1
1684                 ,BTRIM('||sce||')
1685             FROM 
1686                 ' || quote_ident(table_name) || '
1687             WHERE
1688                     NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1689                 AND NULLIF(BTRIM('||sce||'),'''') IS NOT NULL
1690                 AND NOT EXISTS (
1691                     SELECT id
1692                     FROM asset.stat_cat_entry
1693                     WHERE stat_cat = (
1694                         SELECT id
1695                         FROM asset.stat_cat
1696                         WHERE owner = ANY ($2)
1697                         AND BTRIM('||sc||') = BTRIM(name)
1698                     ) AND value = BTRIM('||sce||')
1699                     AND owner = ANY ($2)
1700                 )
1701                 AND NOT EXISTS (
1702                     SELECT id
1703                     FROM asset_stat_cat_entry
1704                     WHERE stat_cat = (
1705                         SELECT id
1706                         FROM asset_stat_cat
1707                         WHERE owner = ANY ($2)
1708                         AND BTRIM('||sc||') = BTRIM(name)
1709                     ) AND value = BTRIM('||sce||')
1710                     AND owner = ANY ($2)
1711                 )
1712             ORDER BY 1,3;'
1713         USING org, org_list;
1714     END;
1715 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1716
1717 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1718     DECLARE
1719         table_schema ALIAS FOR $1;
1720         table_name ALIAS FOR $2;
1721         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1722         org_shortname ALIAS FOR $4;
1723         proceed BOOLEAN;
1724         org INTEGER;
1725         org_list INTEGER[];
1726         o INTEGER;
1727         sc TEXT;
1728         sce TEXT;
1729     BEGIN
1730         SELECT 'desired_sc' || field_suffix INTO sc;
1731         SELECT 'desired_sce' || field_suffix INTO sce;
1732         EXECUTE 'SELECT EXISTS (
1733             SELECT 1
1734             FROM information_schema.columns
1735             WHERE table_schema = $1
1736             AND table_name = $2
1737             and column_name = $3
1738         )' INTO proceed USING table_schema, table_name, sc;
1739         IF NOT proceed THEN
1740             RAISE EXCEPTION 'Missing column %', sc; 
1741         END IF;
1742         EXECUTE 'SELECT EXISTS (
1743             SELECT 1
1744             FROM information_schema.columns
1745             WHERE table_schema = $1
1746             AND table_name = $2
1747             and column_name = $3
1748         )' INTO proceed USING table_schema, table_name, sce;
1749         IF NOT proceed THEN
1750             RAISE EXCEPTION 'Missing column %', sce; 
1751         END IF;
1752
1753         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1754         IF org IS NULL THEN
1755             RAISE EXCEPTION 'Cannot find org by shortname';
1756         END IF;
1757
1758         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1759
1760         EXECUTE 'ALTER TABLE '
1761             || quote_ident(table_name)
1762             || ' DROP COLUMN IF EXISTS x_sc' || field_suffix;
1763         EXECUTE 'ALTER TABLE '
1764             || quote_ident(table_name)
1765             || ' ADD COLUMN x_sc' || field_suffix || ' INTEGER';
1766         EXECUTE 'ALTER TABLE '
1767             || quote_ident(table_name)
1768             || ' DROP COLUMN IF EXISTS x_sce' || field_suffix;
1769         EXECUTE 'ALTER TABLE '
1770             || quote_ident(table_name)
1771             || ' ADD COLUMN x_sce' || field_suffix || ' INTEGER';
1772
1773
1774         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1775             SET
1776                 x_sc' || field_suffix || ' = id
1777             FROM
1778                 (SELECT id, name, owner FROM asset_stat_cat
1779                     UNION SELECT id, name, owner FROM asset.stat_cat) u
1780             WHERE
1781                     BTRIM(UPPER(u.name)) = BTRIM(UPPER(' || sc || '))
1782                 AND u.owner = ANY ($1);'
1783         USING org_list;
1784
1785         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1786             SET
1787                 x_sce' || field_suffix || ' = id
1788             FROM
1789                 (SELECT id, stat_cat, owner, value FROM asset_stat_cat_entry
1790                     UNION SELECT id, stat_cat, owner, value FROM asset.stat_cat_entry) u
1791             WHERE
1792                     u.stat_cat = x_sc' || field_suffix || '
1793                 AND BTRIM(UPPER(u.value)) = BTRIM(UPPER(' || sce || '))
1794                 AND u.owner = ANY ($1);'
1795         USING org_list;
1796
1797         EXECUTE 'SELECT migration_tools.assert(
1798             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sc' || field_suffix || ' <> '''' AND x_sc' || field_suffix || ' IS NULL),
1799             ''Cannot find a desired stat cat'',
1800             ''Found all desired stat cats''
1801         );';
1802
1803         EXECUTE 'SELECT migration_tools.assert(
1804             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sce' || field_suffix || ' <> '''' AND x_sce' || field_suffix || ' IS NULL),
1805             ''Cannot find a desired stat cat entry'',
1806             ''Found all desired stat cat entries''
1807         );';
1808
1809     END;
1810 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1811
1812 -- convenience function for handling item barcode collisions in asset_copy_legacy
1813
1814 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
1815 DECLARE
1816     x_barcode TEXT;
1817     x_id BIGINT;
1818     row_count NUMERIC;
1819     internal_collision_count NUMERIC := 0;
1820     incumbent_collision_count NUMERIC := 0;
1821 BEGIN
1822     FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1823     LOOP
1824         FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
1825         LOOP
1826             UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
1827             GET DIAGNOSTICS row_count = ROW_COUNT;
1828             internal_collision_count := internal_collision_count + row_count;
1829         END LOOP;
1830     END LOOP;
1831     RAISE INFO '% internal collisions', internal_collision_count;
1832     FOR x_barcode IN SELECT a.barcode FROM asset.copy a, asset_copy_legacy b WHERE x_migrate AND a.deleted IS FALSE AND a.barcode = b.barcode
1833     LOOP
1834         FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
1835         LOOP
1836             UPDATE asset_copy_legacy SET barcode = migration_schema || '_incumbent_collision_' || id || '_' || barcode WHERE id = x_id;
1837             GET DIAGNOSTICS row_count = ROW_COUNT;
1838             incumbent_collision_count := incumbent_collision_count + row_count;
1839         END LOOP;
1840     END LOOP;
1841     RAISE INFO '% incumbent collisions', incumbent_collision_count;
1842 END
1843 $function$ LANGUAGE plpgsql;
1844
1845 -- convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
1846 -- this should be ran prior to populating actor_card
1847
1848 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
1849 DECLARE
1850     x_barcode TEXT;
1851     x_id BIGINT;
1852     row_count NUMERIC;
1853     internal_collision_count NUMERIC := 0;
1854     incumbent_barcode_collision_count NUMERIC := 0;
1855     incumbent_usrname_collision_count NUMERIC := 0;
1856 BEGIN
1857     FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1858     LOOP
1859         FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1860         LOOP
1861             UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
1862             GET DIAGNOSTICS row_count = ROW_COUNT;
1863             internal_collision_count := internal_collision_count + row_count;
1864         END LOOP;
1865     END LOOP;
1866     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
1867
1868     FOR x_barcode IN
1869         SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
1870     LOOP
1871         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1872         LOOP
1873             UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_barcode_collision_' || id || '_' || usrname WHERE id = x_id;
1874             GET DIAGNOSTICS row_count = ROW_COUNT;
1875             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
1876         END LOOP;
1877     END LOOP;
1878     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
1879
1880     FOR x_barcode IN
1881         SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
1882     LOOP
1883         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1884         LOOP
1885             UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_usrname_collision_' || id || '_' || usrname WHERE id = x_id;
1886             GET DIAGNOSTICS row_count = ROW_COUNT;
1887             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
1888         END LOOP;
1889     END LOOP;
1890     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
1891 END
1892 $function$ LANGUAGE plpgsql;
1893
1894 -- alternate version: convenience function for handling item barcode collisions in asset_copy_legacy
1895
1896 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
1897 DECLARE
1898     x_barcode TEXT;
1899     x_id BIGINT;
1900     row_count NUMERIC;
1901     internal_collision_count NUMERIC := 0;
1902     incumbent_collision_count NUMERIC := 0;
1903 BEGIN
1904     FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1905     LOOP
1906         FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
1907         LOOP
1908             UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
1909             GET DIAGNOSTICS row_count = ROW_COUNT;
1910             internal_collision_count := internal_collision_count + row_count;
1911         END LOOP;
1912     END LOOP;
1913     RAISE INFO '% internal collisions', internal_collision_count;
1914     FOR x_barcode IN SELECT a.barcode FROM asset.copy a, asset_copy_legacy b WHERE x_migrate AND a.deleted IS FALSE AND a.barcode = b.barcode
1915     LOOP
1916         FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
1917         LOOP
1918             UPDATE asset_copy_legacy SET barcode = migration_schema || '_' || barcode WHERE id = x_id;
1919             GET DIAGNOSTICS row_count = ROW_COUNT;
1920             incumbent_collision_count := incumbent_collision_count + row_count;
1921         END LOOP;
1922     END LOOP;
1923     RAISE INFO '% incumbent collisions', incumbent_collision_count;
1924 END
1925 $function$ LANGUAGE plpgsql;
1926
1927 -- alternate version: convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
1928 -- this should be ran prior to populating actor_card
1929
1930 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
1931 DECLARE
1932     x_barcode TEXT;
1933     x_id BIGINT;
1934     row_count NUMERIC;
1935     internal_collision_count NUMERIC := 0;
1936     incumbent_barcode_collision_count NUMERIC := 0;
1937     incumbent_usrname_collision_count NUMERIC := 0;
1938 BEGIN
1939     FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1940     LOOP
1941         FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1942         LOOP
1943             UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
1944             GET DIAGNOSTICS row_count = ROW_COUNT;
1945             internal_collision_count := internal_collision_count + row_count;
1946         END LOOP;
1947     END LOOP;
1948     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
1949
1950     FOR x_barcode IN
1951         SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
1952     LOOP
1953         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1954         LOOP
1955             UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
1956             GET DIAGNOSTICS row_count = ROW_COUNT;
1957             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
1958         END LOOP;
1959     END LOOP;
1960     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
1961
1962     FOR x_barcode IN
1963         SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
1964     LOOP
1965         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1966         LOOP
1967             UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
1968             GET DIAGNOSTICS row_count = ROW_COUNT;
1969             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
1970         END LOOP;
1971     END LOOP;
1972     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
1973 END
1974 $function$ LANGUAGE plpgsql;
1975
1976 CREATE OR REPLACE FUNCTION migration_tools.is_circ_rule_safe_to_delete( test_matchpoint INTEGER ) RETURNS BOOLEAN AS $func$
1977 -- WARNING: Use at your own risk
1978 -- FIXME: not considering marc_type, marc_form, marc_bib_level, marc_vr_format, usr_age_lower_bound, usr_age_upper_bound, item_age
1979 DECLARE
1980     item_object asset.copy%ROWTYPE;
1981     user_object actor.usr%ROWTYPE;
1982     test_rule_object config.circ_matrix_matchpoint%ROWTYPE;
1983     result_rule_object config.circ_matrix_matchpoint%ROWTYPE;
1984     safe_to_delete BOOLEAN := FALSE;
1985     m action.found_circ_matrix_matchpoint;
1986     n action.found_circ_matrix_matchpoint;
1987     -- ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] )
1988     result_matchpoint INTEGER;
1989 BEGIN
1990     SELECT INTO test_rule_object * FROM config.circ_matrix_matchpoint WHERE id = test_matchpoint;
1991     RAISE INFO 'testing rule: %', test_rule_object;
1992
1993     INSERT INTO actor.usr (
1994         profile,
1995         usrname,
1996         passwd,
1997         ident_type,
1998         first_given_name,
1999         family_name,
2000         home_ou,
2001         juvenile
2002     ) SELECT
2003         COALESCE(test_rule_object.grp, 2),
2004         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
2005         MD5(NOW()::TEXT),
2006         1,
2007         'Ima',
2008         'Test',
2009         COALESCE(test_rule_object.user_home_ou, test_rule_object.org_unit),
2010         COALESCE(test_rule_object.juvenile_flag, FALSE)
2011     ;
2012     
2013     SELECT INTO user_object * FROM actor.usr WHERE id = currval('actor.usr_id_seq');
2014
2015     INSERT INTO asset.call_number (
2016         creator,
2017         editor,
2018         record,
2019         owning_lib,
2020         label,
2021         label_class
2022     ) SELECT
2023         1,
2024         1,
2025         -1,
2026         COALESCE(test_rule_object.copy_owning_lib,test_rule_object.org_unit),
2027         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
2028         1
2029     ;
2030
2031     INSERT INTO asset.copy (
2032         barcode,
2033         circ_lib,
2034         creator,
2035         call_number,
2036         editor,
2037         location,
2038         loan_duration,
2039         fine_level,
2040         ref,
2041         circ_modifier
2042     ) SELECT
2043         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
2044         COALESCE(test_rule_object.copy_circ_lib,test_rule_object.org_unit),
2045         1,
2046         currval('asset.call_number_id_seq'),
2047         1,
2048         COALESCE(test_rule_object.copy_location,1),
2049         2,
2050         2,
2051         COALESCE(test_rule_object.ref_flag,FALSE),
2052         test_rule_object.circ_modifier
2053     ;
2054
2055     SELECT INTO item_object * FROM asset.copy WHERE id = currval('asset.copy_id_seq');
2056
2057     SELECT INTO m * FROM action.find_circ_matrix_matchpoint(
2058         test_rule_object.org_unit,
2059         item_object,
2060         user_object,
2061         COALESCE(test_rule_object.is_renewal,FALSE)
2062     );
2063     RAISE INFO '   action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
2064         test_rule_object.org_unit,
2065         item_object.id,
2066         user_object.id,
2067         COALESCE(test_rule_object.is_renewal,FALSE),
2068         m.success,
2069         m.matchpoint,
2070         m.buildrows
2071     ;
2072
2073     --  disable the rule being tested to see if the outcome changes
2074     UPDATE config.circ_matrix_matchpoint SET active = FALSE WHERE id = (m.matchpoint).id;
2075
2076     SELECT INTO n * FROM action.find_circ_matrix_matchpoint(
2077         test_rule_object.org_unit,
2078         item_object,
2079         user_object,
2080         COALESCE(test_rule_object.is_renewal,FALSE)
2081     );
2082     RAISE INFO 'VS action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
2083         test_rule_object.org_unit,
2084         item_object.id,
2085         user_object.id,
2086         COALESCE(test_rule_object.is_renewal,FALSE),
2087         n.success,
2088         n.matchpoint,
2089         n.buildrows
2090     ;
2091
2092     -- FIXME: We could dig deeper and see if the referenced config.rule_*
2093     -- entries are effectively equivalent, but for now, let's assume no
2094     -- duplicate rules at that level
2095     IF (
2096             (m.matchpoint).circulate = (n.matchpoint).circulate
2097         AND (m.matchpoint).duration_rule = (n.matchpoint).duration_rule
2098         AND (m.matchpoint).recurring_fine_rule = (n.matchpoint).recurring_fine_rule
2099         AND (m.matchpoint).max_fine_rule = (n.matchpoint).max_fine_rule
2100         AND (
2101                 (m.matchpoint).hard_due_date = (n.matchpoint).hard_due_date
2102                 OR (
2103                         (m.matchpoint).hard_due_date IS NULL
2104                     AND (n.matchpoint).hard_due_date IS NULL
2105                 )
2106         )
2107         AND (
2108                 (m.matchpoint).renewals = (n.matchpoint).renewals
2109                 OR (
2110                         (m.matchpoint).renewals IS NULL
2111                     AND (n.matchpoint).renewals IS NULL
2112                 )
2113         )
2114         AND (
2115                 (m.matchpoint).grace_period = (n.matchpoint).grace_period
2116                 OR (
2117                         (m.matchpoint).grace_period IS NULL
2118                     AND (n.matchpoint).grace_period IS NULL
2119                 )
2120         )
2121         AND (
2122                 (m.matchpoint).total_copy_hold_ratio = (n.matchpoint).total_copy_hold_ratio
2123                 OR (
2124                         (m.matchpoint).total_copy_hold_ratio IS NULL
2125                     AND (n.matchpoint).total_copy_hold_ratio IS NULL
2126                 )
2127         )
2128         AND (
2129                 (m.matchpoint).available_copy_hold_ratio = (n.matchpoint).available_copy_hold_ratio
2130                 OR (
2131                         (m.matchpoint).available_copy_hold_ratio IS NULL
2132                     AND (n.matchpoint).available_copy_hold_ratio IS NULL
2133                 )
2134         )
2135         AND NOT EXISTS (
2136             SELECT limit_set, fallthrough
2137             FROM config.circ_matrix_limit_set_map
2138             WHERE active and matchpoint = (m.matchpoint).id
2139             EXCEPT
2140             SELECT limit_set, fallthrough
2141             FROM config.circ_matrix_limit_set_map
2142             WHERE active and matchpoint = (n.matchpoint).id
2143         )
2144
2145     ) THEN
2146         RAISE INFO 'rule has same outcome';
2147         safe_to_delete := TRUE;
2148     ELSE
2149         RAISE INFO 'rule has different outcome';
2150         safe_to_delete := FALSE;
2151     END IF;
2152
2153     RAISE EXCEPTION 'rollback the temporary changes';
2154
2155 EXCEPTION WHEN OTHERS THEN
2156
2157     RAISE INFO 'inside exception block: %, %', SQLSTATE, SQLERRM;
2158     RETURN safe_to_delete;
2159
2160 END;
2161 $func$ LANGUAGE plpgsql;
2162