b9c7543c82c1d03386f042e00c4e945c63186a01
[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 -- convenience functions for handling copy_location maps
842 CREATE OR REPLACE FUNCTION migration_tools.handle_shelf (TEXT,TEXT,TEXT,INTEGER) RETURNS VOID AS $$
843     SELECT migration_tools._handle_shelf($1,$2,$3,$4,TRUE);
844 $$ LANGUAGE SQL;
845
846 CREATE OR REPLACE FUNCTION migration_tools._handle_shelf (TEXT,TEXT,TEXT,INTEGER,BOOLEAN) RETURNS VOID AS $$
847     DECLARE
848         table_schema ALIAS FOR $1;
849         table_name ALIAS FOR $2;
850         org_shortname ALIAS FOR $3;
851         org_range ALIAS FOR $4;
852         make_assertion ALIAS FOR $5;
853         proceed BOOLEAN;
854         org INTEGER;
855         -- if x_org is on the mapping table, it'll take precedence over the passed org_shortname param
856         -- though we'll still use the passed org for the full path traversal when needed
857         x_org_found BOOLEAN;
858         x_org INTEGER;
859         org_list INTEGER[];
860         o INTEGER;
861         row_count NUMERIC;
862     BEGIN
863         EXECUTE 'SELECT EXISTS (
864             SELECT 1
865             FROM information_schema.columns
866             WHERE table_schema = $1
867             AND table_name = $2
868             and column_name = ''desired_shelf''
869         )' INTO proceed USING table_schema, table_name;
870         IF NOT proceed THEN
871             RAISE EXCEPTION 'Missing column desired_shelf';
872         END IF;
873
874         EXECUTE 'SELECT EXISTS (
875             SELECT 1
876             FROM information_schema.columns
877             WHERE table_schema = $1
878             AND table_name = $2
879             and column_name = ''x_org''
880         )' INTO x_org_found USING table_schema, table_name;
881
882         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
883         IF org IS NULL THEN
884             RAISE EXCEPTION 'Cannot find org by shortname';
885         END IF;
886
887         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
888
889         EXECUTE 'ALTER TABLE '
890             || quote_ident(table_name)
891             || ' DROP COLUMN IF EXISTS x_shelf';
892         EXECUTE 'ALTER TABLE '
893             || quote_ident(table_name)
894             || ' ADD COLUMN x_shelf INTEGER';
895
896         IF x_org_found THEN
897             RAISE INFO 'Found x_org column';
898             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
899                 || ' SET x_shelf = b.id FROM asset_copy_location b'
900                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
901                 || ' AND b.owning_lib = x_org'
902                 || ' AND NOT b.deleted';
903             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
904                 || ' SET x_shelf = b.id FROM asset.copy_location b'
905                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
906                 || ' AND b.owning_lib = x_org'
907                 || ' AND x_shelf IS NULL'
908                 || ' AND NOT b.deleted';
909         ELSE
910             RAISE INFO 'Did not find x_org column';
911             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
912                 || ' SET x_shelf = b.id FROM asset_copy_location b'
913                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
914                 || ' AND b.owning_lib = $1'
915                 || ' AND NOT b.deleted'
916             USING org;
917             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
918                 || ' SET x_shelf = b.id FROM asset_copy_location b'
919                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
920                 || ' AND b.owning_lib = $1'
921                 || ' AND x_shelf IS NULL'
922                 || ' AND NOT b.deleted'
923             USING org;
924         END IF;
925
926         FOREACH o IN ARRAY org_list LOOP
927             RAISE INFO 'Considering org %', o;
928             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
929                 || ' SET x_shelf = b.id FROM asset.copy_location b'
930                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
931                 || ' AND b.owning_lib = $1 AND x_shelf IS NULL'
932                 || ' AND NOT b.deleted'
933             USING o;
934             GET DIAGNOSTICS row_count = ROW_COUNT;
935             RAISE INFO 'Updated % rows', row_count;
936         END LOOP;
937
938         IF make_assertion THEN
939             EXECUTE 'SELECT migration_tools.assert(
940                 NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_shelf <> '''' AND x_shelf IS NULL),
941                 ''Cannot find a desired location'',
942                 ''Found all desired locations''
943             );';
944         END IF;
945
946     END;
947 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
948
949 -- convenience functions for handling circmod maps
950
951 CREATE OR REPLACE FUNCTION migration_tools.handle_circmod (TEXT,TEXT) RETURNS VOID AS $$
952     DECLARE
953         table_schema ALIAS FOR $1;
954         table_name ALIAS FOR $2;
955         proceed BOOLEAN;
956     BEGIN
957         EXECUTE 'SELECT EXISTS (
958             SELECT 1
959             FROM information_schema.columns
960             WHERE table_schema = $1
961             AND table_name = $2
962             and column_name = ''desired_circmod''
963         )' INTO proceed USING table_schema, table_name;
964         IF NOT proceed THEN
965             RAISE EXCEPTION 'Missing column desired_circmod'; 
966         END IF;
967
968         EXECUTE 'ALTER TABLE '
969             || quote_ident(table_name)
970             || ' DROP COLUMN IF EXISTS x_circmod';
971         EXECUTE 'ALTER TABLE '
972             || quote_ident(table_name)
973             || ' ADD COLUMN x_circmod TEXT';
974
975         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
976             || ' SET x_circmod = code FROM config.circ_modifier b'
977             || ' WHERE BTRIM(UPPER(a.desired_circmod)) = BTRIM(UPPER(b.code))';
978
979         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
980             || ' SET x_circmod = code FROM config.circ_modifier b'
981             || ' WHERE BTRIM(UPPER(a.desired_circmod)) = BTRIM(UPPER(b.name))'
982             || ' AND x_circmod IS NULL';
983
984         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
985             || ' SET x_circmod = code FROM config.circ_modifier b'
986             || ' WHERE BTRIM(UPPER(a.desired_circmod)) = BTRIM(UPPER(b.description))'
987             || ' AND x_circmod IS NULL';
988
989         EXECUTE 'SELECT migration_tools.assert(
990             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_circmod <> '''' AND x_circmod IS NULL),
991             ''Cannot find a desired circulation modifier'',
992             ''Found all desired circulation modifiers''
993         );';
994
995     END;
996 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
997
998 -- convenience functions for handling item status maps
999
1000 CREATE OR REPLACE FUNCTION migration_tools.handle_status (TEXT,TEXT) RETURNS VOID AS $$
1001     DECLARE
1002         table_schema ALIAS FOR $1;
1003         table_name ALIAS FOR $2;
1004         proceed BOOLEAN;
1005     BEGIN
1006         EXECUTE 'SELECT EXISTS (
1007             SELECT 1
1008             FROM information_schema.columns
1009             WHERE table_schema = $1
1010             AND table_name = $2
1011             and column_name = ''desired_status''
1012         )' INTO proceed USING table_schema, table_name;
1013         IF NOT proceed THEN
1014             RAISE EXCEPTION 'Missing column desired_status'; 
1015         END IF;
1016
1017         EXECUTE 'ALTER TABLE '
1018             || quote_ident(table_name)
1019             || ' DROP COLUMN IF EXISTS x_status';
1020         EXECUTE 'ALTER TABLE '
1021             || quote_ident(table_name)
1022             || ' ADD COLUMN x_status INTEGER';
1023
1024         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1025             || ' SET x_status = id FROM config.copy_status b'
1026             || ' WHERE BTRIM(UPPER(a.desired_status)) = BTRIM(UPPER(b.name))';
1027
1028         EXECUTE 'SELECT migration_tools.assert(
1029             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_status <> '''' AND x_status IS NULL),
1030             ''Cannot find a desired copy status'',
1031             ''Found all desired copy statuses''
1032         );';
1033
1034     END;
1035 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1036
1037 -- convenience functions for handling org maps
1038
1039 CREATE OR REPLACE FUNCTION migration_tools.handle_org (TEXT,TEXT) RETURNS VOID AS $$
1040     DECLARE
1041         table_schema ALIAS FOR $1;
1042         table_name ALIAS FOR $2;
1043         proceed BOOLEAN;
1044     BEGIN
1045         EXECUTE 'SELECT EXISTS (
1046             SELECT 1
1047             FROM information_schema.columns
1048             WHERE table_schema = $1
1049             AND table_name = $2
1050             and column_name = ''desired_org''
1051         )' INTO proceed USING table_schema, table_name;
1052         IF NOT proceed THEN
1053             RAISE EXCEPTION 'Missing column desired_org'; 
1054         END IF;
1055
1056         EXECUTE 'ALTER TABLE '
1057             || quote_ident(table_name)
1058             || ' DROP COLUMN IF EXISTS x_org';
1059         EXECUTE 'ALTER TABLE '
1060             || quote_ident(table_name)
1061             || ' ADD COLUMN x_org INTEGER';
1062
1063         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1064             || ' SET x_org = b.id FROM actor.org_unit b'
1065             || ' WHERE BTRIM(a.desired_org) = BTRIM(b.shortname)';
1066
1067         EXECUTE 'SELECT migration_tools.assert(
1068             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_org <> '''' AND x_org IS NULL),
1069             ''Cannot find a desired org unit'',
1070             ''Found all desired org units''
1071         );';
1072
1073     END;
1074 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1075
1076 -- convenience function for handling desired_not_migrate
1077
1078 CREATE OR REPLACE FUNCTION migration_tools.handle_not_migrate (TEXT,TEXT) RETURNS VOID AS $$
1079     DECLARE
1080         table_schema ALIAS FOR $1;
1081         table_name ALIAS FOR $2;
1082         proceed BOOLEAN;
1083     BEGIN
1084         EXECUTE 'SELECT EXISTS (
1085             SELECT 1
1086             FROM information_schema.columns
1087             WHERE table_schema = $1
1088             AND table_name = $2
1089             and column_name = ''desired_not_migrate''
1090         )' INTO proceed USING table_schema, table_name;
1091         IF NOT proceed THEN
1092             RAISE EXCEPTION 'Missing column desired_not_migrate'; 
1093         END IF;
1094
1095         EXECUTE 'ALTER TABLE '
1096             || quote_ident(table_name)
1097             || ' DROP COLUMN IF EXISTS x_migrate';
1098         EXECUTE 'ALTER TABLE '
1099             || quote_ident(table_name)
1100             || ' ADD COLUMN x_migrate BOOLEAN';
1101
1102         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1103             || ' SET x_migrate = CASE'
1104             || ' WHEN BTRIM(desired_not_migrate) = ''TRUE'' THEN FALSE'
1105             || ' WHEN BTRIM(desired_not_migrate) = ''DNM'' THEN FALSE'
1106             || ' WHEN BTRIM(desired_not_migrate) = ''Do Not Migrate'' THEN FALSE'
1107             || ' WHEN BTRIM(desired_not_migrate) = ''FALSE'' THEN TRUE'
1108             || ' WHEN BTRIM(desired_not_migrate) = ''Migrate'' THEN TRUE'
1109             || ' WHEN BTRIM(desired_not_migrate) = '''' THEN TRUE'
1110             || ' END';
1111
1112         EXECUTE 'SELECT migration_tools.assert(
1113             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE x_migrate IS NULL),
1114             ''Not all desired_not_migrate values understood'',
1115             ''All desired_not_migrate values understood''
1116         );';
1117
1118     END;
1119 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1120
1121 -- convenience function for handling desired_not_migrate
1122
1123 CREATE OR REPLACE FUNCTION migration_tools.handle_barred_or_blocked (TEXT,TEXT) RETURNS VOID AS $$
1124     DECLARE
1125         table_schema ALIAS FOR $1;
1126         table_name ALIAS FOR $2;
1127         proceed BOOLEAN;
1128     BEGIN
1129         EXECUTE 'SELECT EXISTS (
1130             SELECT 1
1131             FROM information_schema.columns
1132             WHERE table_schema = $1
1133             AND table_name = $2
1134             and column_name = ''desired_barred_or_blocked''
1135         )' INTO proceed USING table_schema, table_name;
1136         IF NOT proceed THEN
1137             RAISE EXCEPTION 'Missing column desired_barred_or_blocked'; 
1138         END IF;
1139
1140         EXECUTE 'ALTER TABLE '
1141             || quote_ident(table_name)
1142             || ' DROP COLUMN IF EXISTS x_barred';
1143         EXECUTE 'ALTER TABLE '
1144             || quote_ident(table_name)
1145             || ' ADD COLUMN x_barred BOOLEAN';
1146
1147         EXECUTE 'ALTER TABLE '
1148             || quote_ident(table_name)
1149             || ' DROP COLUMN IF EXISTS x_blocked';
1150         EXECUTE 'ALTER TABLE '
1151             || quote_ident(table_name)
1152             || ' ADD COLUMN x_blocked BOOLEAN';
1153
1154         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1155             || ' SET x_barred = CASE'
1156             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Barred'' THEN TRUE'
1157             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Blocked'' THEN FALSE'
1158             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Neither'' THEN FALSE'
1159             || ' WHEN BTRIM(desired_barred_or_blocked) = '''' THEN FALSE'
1160             || ' END';
1161
1162         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1163             || ' SET x_blocked = CASE'
1164             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Blocked'' THEN TRUE'
1165             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Barred'' THEN FALSE'
1166             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Neither'' THEN FALSE'
1167             || ' WHEN BTRIM(desired_barred_or_blocked) = '''' THEN FALSE'
1168             || ' END';
1169
1170         EXECUTE 'SELECT migration_tools.assert(
1171             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE x_barred IS NULL or x_blocked IS NULL),
1172             ''Not all desired_barred_or_blocked values understood'',
1173             ''All desired_barred_or_blocked values understood''
1174         );';
1175
1176     END;
1177 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1178
1179 -- convenience function for handling desired_profile
1180
1181 CREATE OR REPLACE FUNCTION migration_tools.handle_profile (TEXT,TEXT) RETURNS VOID AS $$
1182     DECLARE
1183         table_schema ALIAS FOR $1;
1184         table_name ALIAS FOR $2;
1185         proceed BOOLEAN;
1186     BEGIN
1187         EXECUTE 'SELECT EXISTS (
1188             SELECT 1
1189             FROM information_schema.columns
1190             WHERE table_schema = $1
1191             AND table_name = $2
1192             and column_name = ''desired_profile''
1193         )' INTO proceed USING table_schema, table_name;
1194         IF NOT proceed THEN
1195             RAISE EXCEPTION 'Missing column desired_profile'; 
1196         END IF;
1197
1198         EXECUTE 'ALTER TABLE '
1199             || quote_ident(table_name)
1200             || ' DROP COLUMN IF EXISTS x_profile';
1201         EXECUTE 'ALTER TABLE '
1202             || quote_ident(table_name)
1203             || ' ADD COLUMN x_profile INTEGER';
1204
1205         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1206             || ' SET x_profile = b.id FROM permission.grp_tree b'
1207             || ' WHERE BTRIM(UPPER(a.desired_profile)) = BTRIM(UPPER(b.name))';
1208
1209         EXECUTE 'SELECT migration_tools.assert(
1210             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_profile <> '''' AND x_profile IS NULL),
1211             ''Cannot find a desired profile'',
1212             ''Found all desired profiles''
1213         );';
1214
1215     END;
1216 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1217
1218 -- convenience function for handling desired actor stat cats
1219
1220 CREATE OR REPLACE FUNCTION migration_tools.vivicate_actor_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1221     DECLARE
1222         table_schema ALIAS FOR $1;
1223         table_name ALIAS FOR $2;
1224         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1225         org_shortname ALIAS FOR $4;
1226         proceed BOOLEAN;
1227         org INTEGER;
1228         org_list INTEGER[];
1229         sc TEXT;
1230         sce TEXT;
1231     BEGIN
1232
1233         SELECT 'desired_sc' || field_suffix INTO sc;
1234         SELECT 'desired_sce' || field_suffix INTO sce;
1235
1236         EXECUTE 'SELECT EXISTS (
1237             SELECT 1
1238             FROM information_schema.columns
1239             WHERE table_schema = $1
1240             AND table_name = $2
1241             and column_name = $3
1242         )' INTO proceed USING table_schema, table_name, sc;
1243         IF NOT proceed THEN
1244             RAISE EXCEPTION 'Missing column %', sc; 
1245         END IF;
1246         EXECUTE 'SELECT EXISTS (
1247             SELECT 1
1248             FROM information_schema.columns
1249             WHERE table_schema = $1
1250             AND table_name = $2
1251             and column_name = $3
1252         )' INTO proceed USING table_schema, table_name, sce;
1253         IF NOT proceed THEN
1254             RAISE EXCEPTION 'Missing column %', sce; 
1255         END IF;
1256
1257         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1258         IF org IS NULL THEN
1259             RAISE EXCEPTION 'Cannot find org by shortname';
1260         END IF;
1261         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1262
1263         -- caller responsible for their own truncates though we try to prevent duplicates
1264         EXECUTE 'INSERT INTO actor_stat_cat (owner, name)
1265             SELECT DISTINCT
1266                  $1
1267                 ,BTRIM('||sc||')
1268             FROM 
1269                 ' || quote_ident(table_name) || '
1270             WHERE
1271                 NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1272                 AND NOT EXISTS (
1273                     SELECT id
1274                     FROM actor.stat_cat
1275                     WHERE owner = ANY ($2)
1276                     AND name = BTRIM('||sc||')
1277                 )
1278                 AND NOT EXISTS (
1279                     SELECT id
1280                     FROM actor_stat_cat
1281                     WHERE owner = ANY ($2)
1282                     AND name = BTRIM('||sc||')
1283                 )
1284             ORDER BY 2;'
1285         USING org, org_list;
1286
1287         EXECUTE 'INSERT INTO actor_stat_cat_entry (stat_cat, owner, value)
1288             SELECT DISTINCT
1289                 COALESCE(
1290                     (SELECT id
1291                         FROM actor.stat_cat
1292                         WHERE owner = ANY ($2)
1293                         AND BTRIM('||sc||') = BTRIM(name))
1294                    ,(SELECT id
1295                         FROM actor_stat_cat
1296                         WHERE owner = ANY ($2)
1297                         AND BTRIM('||sc||') = BTRIM(name))
1298                 )
1299                 ,$1
1300                 ,BTRIM('||sce||')
1301             FROM 
1302                 ' || quote_ident(table_name) || '
1303             WHERE
1304                     NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1305                 AND NULLIF(BTRIM('||sce||'),'''') IS NOT NULL
1306                 AND NOT EXISTS (
1307                     SELECT id
1308                     FROM actor.stat_cat_entry
1309                     WHERE stat_cat = (
1310                         SELECT id
1311                         FROM actor.stat_cat
1312                         WHERE owner = ANY ($2)
1313                         AND BTRIM('||sc||') = BTRIM(name)
1314                     ) AND value = BTRIM('||sce||')
1315                     AND owner = ANY ($2)
1316                 )
1317                 AND NOT EXISTS (
1318                     SELECT id
1319                     FROM actor_stat_cat_entry
1320                     WHERE stat_cat = (
1321                         SELECT id
1322                         FROM actor_stat_cat
1323                         WHERE owner = ANY ($2)
1324                         AND BTRIM('||sc||') = BTRIM(name)
1325                     ) AND value = BTRIM('||sce||')
1326                     AND owner = ANY ($2)
1327                 )
1328             ORDER BY 1,3;'
1329         USING org, org_list;
1330     END;
1331 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1332
1333 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1334     DECLARE
1335         table_schema ALIAS FOR $1;
1336         table_name ALIAS FOR $2;
1337         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1338         org_shortname ALIAS FOR $4;
1339         proceed BOOLEAN;
1340         org INTEGER;
1341         org_list INTEGER[];
1342         o INTEGER;
1343         sc TEXT;
1344         sce TEXT;
1345     BEGIN
1346         SELECT 'desired_sc' || field_suffix INTO sc;
1347         SELECT 'desired_sce' || field_suffix INTO sce;
1348         EXECUTE 'SELECT EXISTS (
1349             SELECT 1
1350             FROM information_schema.columns
1351             WHERE table_schema = $1
1352             AND table_name = $2
1353             and column_name = $3
1354         )' INTO proceed USING table_schema, table_name, sc;
1355         IF NOT proceed THEN
1356             RAISE EXCEPTION 'Missing column %', sc; 
1357         END IF;
1358         EXECUTE 'SELECT EXISTS (
1359             SELECT 1
1360             FROM information_schema.columns
1361             WHERE table_schema = $1
1362             AND table_name = $2
1363             and column_name = $3
1364         )' INTO proceed USING table_schema, table_name, sce;
1365         IF NOT proceed THEN
1366             RAISE EXCEPTION 'Missing column %', sce; 
1367         END IF;
1368
1369         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1370         IF org IS NULL THEN
1371             RAISE EXCEPTION 'Cannot find org by shortname';
1372         END IF;
1373
1374         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1375
1376         EXECUTE 'ALTER TABLE '
1377             || quote_ident(table_name)
1378             || ' DROP COLUMN IF EXISTS x_sc' || field_suffix;
1379         EXECUTE 'ALTER TABLE '
1380             || quote_ident(table_name)
1381             || ' ADD COLUMN x_sc' || field_suffix || ' INTEGER';
1382         EXECUTE 'ALTER TABLE '
1383             || quote_ident(table_name)
1384             || ' DROP COLUMN IF EXISTS x_sce' || field_suffix;
1385         EXECUTE 'ALTER TABLE '
1386             || quote_ident(table_name)
1387             || ' ADD COLUMN x_sce' || field_suffix || ' INTEGER';
1388
1389
1390         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1391             SET
1392                 x_sc' || field_suffix || ' = id
1393             FROM
1394                 (SELECT id, name, owner FROM actor_stat_cat
1395                     UNION SELECT id, name, owner FROM actor.stat_cat) u
1396             WHERE
1397                     BTRIM(UPPER(u.name)) = BTRIM(UPPER(' || sc || '))
1398                 AND u.owner = ANY ($1);'
1399         USING org_list;
1400
1401         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1402             SET
1403                 x_sce' || field_suffix || ' = id
1404             FROM
1405                 (SELECT id, stat_cat, owner, value FROM actor_stat_cat_entry
1406                     UNION SELECT id, stat_cat, owner, value FROM actor.stat_cat_entry) u
1407             WHERE
1408                     u.stat_cat = x_sc' || field_suffix || '
1409                 AND BTRIM(UPPER(u.value)) = BTRIM(UPPER(' || sce || '))
1410                 AND u.owner = ANY ($1);'
1411         USING org_list;
1412
1413         EXECUTE 'SELECT migration_tools.assert(
1414             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sc' || field_suffix || ' <> '''' AND x_sc' || field_suffix || ' IS NULL),
1415             ''Cannot find a desired stat cat'',
1416             ''Found all desired stat cats''
1417         );';
1418
1419         EXECUTE 'SELECT migration_tools.assert(
1420             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sce' || field_suffix || ' <> '''' AND x_sce' || field_suffix || ' IS NULL),
1421             ''Cannot find a desired stat cat entry'',
1422             ''Found all desired stat cat entries''
1423         );';
1424
1425     END;
1426 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1427
1428 -- convenience functions for adding shelving locations
1429 DROP FUNCTION IF EXISTS migration_tools.find_shelf(INT,TEXT);
1430 CREATE OR REPLACE FUNCTION migration_tools.find_shelf(org_id INT, shelf_name TEXT) RETURNS INTEGER AS $$
1431 DECLARE
1432     return_id   INT;
1433     d           INT;
1434     cur_id      INT;
1435 BEGIN
1436     SELECT INTO d MAX(distance) FROM actor.org_unit_ancestors_distance(org_id);
1437     WHILE d >= 0
1438     LOOP
1439         SELECT INTO cur_id id FROM actor.org_unit_ancestor_at_depth(org_id,d);
1440         SELECT INTO return_id id FROM asset.copy_location WHERE owning_lib = cur_id AND name ILIKE shelf_name;
1441         IF return_id IS NOT NULL THEN
1442                 RETURN return_id;
1443         END IF;
1444         d := d - 1;
1445     END LOOP;
1446
1447     RETURN NULL;
1448 END
1449 $$ LANGUAGE plpgsql;
1450
1451 -- may remove later but testing using this with new migration scripts and not loading acls until go live
1452
1453 DROP FUNCTION IF EXISTS migration_tools.find_mig_shelf(INT,TEXT);
1454 CREATE OR REPLACE FUNCTION migration_tools.find_mig_shelf(org_id INT, shelf_name TEXT) RETURNS INTEGER AS $$
1455 DECLARE
1456     return_id   INT;
1457     d           INT;
1458     cur_id      INT;
1459 BEGIN
1460     SELECT INTO d MAX(distance) FROM actor.org_unit_ancestors_distance(org_id);
1461     WHILE d >= 0
1462     LOOP
1463         SELECT INTO cur_id id FROM actor.org_unit_ancestor_at_depth(org_id,d);
1464         
1465         SELECT INTO return_id id FROM 
1466             (SELECT * FROM asset.copy_location UNION ALL SELECT * FROM asset_copy_location) x
1467             WHERE owning_lib = cur_id AND name ILIKE shelf_name;
1468         IF return_id IS NOT NULL THEN
1469                 RETURN return_id;
1470         END IF;
1471         d := d - 1;
1472     END LOOP;
1473
1474     RETURN NULL;
1475 END
1476 $$ LANGUAGE plpgsql;
1477
1478 -- convenience function for linking to the item staging table
1479
1480 CREATE OR REPLACE FUNCTION migration_tools.handle_item_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
1481     DECLARE
1482         table_schema ALIAS FOR $1;
1483         table_name ALIAS FOR $2;
1484         foreign_column_name ALIAS FOR $3;
1485         main_column_name ALIAS FOR $4;
1486         btrim_desired ALIAS FOR $5;
1487         proceed BOOLEAN;
1488     BEGIN
1489         EXECUTE 'SELECT EXISTS (
1490             SELECT 1
1491             FROM information_schema.columns
1492             WHERE table_schema = $1
1493             AND table_name = $2
1494             and column_name = $3
1495         )' INTO proceed USING table_schema, table_name, foreign_column_name;
1496         IF NOT proceed THEN
1497             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
1498         END IF;
1499
1500         EXECUTE 'SELECT EXISTS (
1501             SELECT 1
1502             FROM information_schema.columns
1503             WHERE table_schema = $1
1504             AND table_name = ''asset_copy_legacy''
1505             and column_name = $2
1506         )' INTO proceed USING table_schema, main_column_name;
1507         IF NOT proceed THEN
1508             RAISE EXCEPTION 'No %.asset_copy_legacy with column %', table_schema, main_column_name; 
1509         END IF;
1510
1511         EXECUTE 'ALTER TABLE '
1512             || quote_ident(table_name)
1513             || ' DROP COLUMN IF EXISTS x_item';
1514         EXECUTE 'ALTER TABLE '
1515             || quote_ident(table_name)
1516             || ' ADD COLUMN x_item BIGINT';
1517
1518         IF btrim_desired THEN
1519             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1520                 || ' SET x_item = b.id FROM asset_copy_legacy b'
1521                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
1522                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
1523         ELSE
1524             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1525                 || ' SET x_item = b.id FROM asset_copy_legacy b'
1526                 || ' WHERE a.' || quote_ident(foreign_column_name)
1527                 || ' = b.' || quote_ident(main_column_name);
1528         END IF;
1529
1530         --EXECUTE 'SELECT migration_tools.assert(
1531         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_item IS NULL),
1532         --    ''Cannot link every barcode'',
1533         --    ''Every barcode linked''
1534         --);';
1535
1536     END;
1537 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1538
1539 -- convenience function for linking to the user staging table
1540
1541 CREATE OR REPLACE FUNCTION migration_tools.handle_user_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
1542     DECLARE
1543         table_schema ALIAS FOR $1;
1544         table_name ALIAS FOR $2;
1545         foreign_column_name ALIAS FOR $3;
1546         main_column_name ALIAS FOR $4;
1547         btrim_desired ALIAS FOR $5;
1548         proceed BOOLEAN;
1549     BEGIN
1550         EXECUTE 'SELECT EXISTS (
1551             SELECT 1
1552             FROM information_schema.columns
1553             WHERE table_schema = $1
1554             AND table_name = $2
1555             and column_name = $3
1556         )' INTO proceed USING table_schema, table_name, foreign_column_name;
1557         IF NOT proceed THEN
1558             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
1559         END IF;
1560
1561         EXECUTE 'SELECT EXISTS (
1562             SELECT 1
1563             FROM information_schema.columns
1564             WHERE table_schema = $1
1565             AND table_name = ''actor_usr_legacy''
1566             and column_name = $2
1567         )' INTO proceed USING table_schema, main_column_name;
1568         IF NOT proceed THEN
1569             RAISE EXCEPTION 'No %.actor_usr_legacy with column %', table_schema, main_column_name; 
1570         END IF;
1571
1572         EXECUTE 'ALTER TABLE '
1573             || quote_ident(table_name)
1574             || ' DROP COLUMN IF EXISTS x_user';
1575         EXECUTE 'ALTER TABLE '
1576             || quote_ident(table_name)
1577             || ' ADD COLUMN x_user INTEGER';
1578
1579         IF btrim_desired THEN
1580             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1581                 || ' SET x_user = b.id FROM actor_usr_legacy b'
1582                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
1583                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
1584         ELSE
1585             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1586                 || ' SET x_user = b.id FROM actor_usr_legacy b'
1587                 || ' WHERE a.' || quote_ident(foreign_column_name)
1588                 || ' = b.' || quote_ident(main_column_name);
1589         END IF;
1590
1591         --EXECUTE 'SELECT migration_tools.assert(
1592         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_user IS NULL),
1593         --    ''Cannot link every barcode'',
1594         --    ''Every barcode linked''
1595         --);';
1596
1597     END;
1598 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1599
1600 -- convenience function for handling desired asset stat cats
1601
1602 CREATE OR REPLACE FUNCTION migration_tools.vivicate_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1603     DECLARE
1604         table_schema ALIAS FOR $1;
1605         table_name ALIAS FOR $2;
1606         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1607         org_shortname ALIAS FOR $4;
1608         proceed BOOLEAN;
1609         org INTEGER;
1610         org_list INTEGER[];
1611         sc TEXT;
1612         sce TEXT;
1613     BEGIN
1614
1615         SELECT 'desired_sc' || field_suffix INTO sc;
1616         SELECT 'desired_sce' || field_suffix INTO sce;
1617
1618         EXECUTE 'SELECT EXISTS (
1619             SELECT 1
1620             FROM information_schema.columns
1621             WHERE table_schema = $1
1622             AND table_name = $2
1623             and column_name = $3
1624         )' INTO proceed USING table_schema, table_name, sc;
1625         IF NOT proceed THEN
1626             RAISE EXCEPTION 'Missing column %', sc; 
1627         END IF;
1628         EXECUTE 'SELECT EXISTS (
1629             SELECT 1
1630             FROM information_schema.columns
1631             WHERE table_schema = $1
1632             AND table_name = $2
1633             and column_name = $3
1634         )' INTO proceed USING table_schema, table_name, sce;
1635         IF NOT proceed THEN
1636             RAISE EXCEPTION 'Missing column %', sce; 
1637         END IF;
1638
1639         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1640         IF org IS NULL THEN
1641             RAISE EXCEPTION 'Cannot find org by shortname';
1642         END IF;
1643         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1644
1645         -- caller responsible for their own truncates though we try to prevent duplicates
1646         EXECUTE 'INSERT INTO asset_stat_cat (owner, name)
1647             SELECT DISTINCT
1648                  $1
1649                 ,BTRIM('||sc||')
1650             FROM 
1651                 ' || quote_ident(table_name) || '
1652             WHERE
1653                 NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1654                 AND NOT EXISTS (
1655                     SELECT id
1656                     FROM asset.stat_cat
1657                     WHERE owner = ANY ($2)
1658                     AND name = BTRIM('||sc||')
1659                 )
1660                 AND NOT EXISTS (
1661                     SELECT id
1662                     FROM asset_stat_cat
1663                     WHERE owner = ANY ($2)
1664                     AND name = BTRIM('||sc||')
1665                 )
1666             ORDER BY 2;'
1667         USING org, org_list;
1668
1669         EXECUTE 'INSERT INTO asset_stat_cat_entry (stat_cat, owner, value)
1670             SELECT DISTINCT
1671                 COALESCE(
1672                     (SELECT id
1673                         FROM asset.stat_cat
1674                         WHERE owner = ANY ($2)
1675                         AND BTRIM('||sc||') = BTRIM(name))
1676                    ,(SELECT id
1677                         FROM asset_stat_cat
1678                         WHERE owner = ANY ($2)
1679                         AND BTRIM('||sc||') = BTRIM(name))
1680                 )
1681                 ,$1
1682                 ,BTRIM('||sce||')
1683             FROM 
1684                 ' || quote_ident(table_name) || '
1685             WHERE
1686                     NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1687                 AND NULLIF(BTRIM('||sce||'),'''') IS NOT NULL
1688                 AND NOT EXISTS (
1689                     SELECT id
1690                     FROM asset.stat_cat_entry
1691                     WHERE stat_cat = (
1692                         SELECT id
1693                         FROM asset.stat_cat
1694                         WHERE owner = ANY ($2)
1695                         AND BTRIM('||sc||') = BTRIM(name)
1696                     ) AND value = BTRIM('||sce||')
1697                     AND owner = ANY ($2)
1698                 )
1699                 AND NOT EXISTS (
1700                     SELECT id
1701                     FROM asset_stat_cat_entry
1702                     WHERE stat_cat = (
1703                         SELECT id
1704                         FROM asset_stat_cat
1705                         WHERE owner = ANY ($2)
1706                         AND BTRIM('||sc||') = BTRIM(name)
1707                     ) AND value = BTRIM('||sce||')
1708                     AND owner = ANY ($2)
1709                 )
1710             ORDER BY 1,3;'
1711         USING org, org_list;
1712     END;
1713 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1714
1715 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1716     DECLARE
1717         table_schema ALIAS FOR $1;
1718         table_name ALIAS FOR $2;
1719         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1720         org_shortname ALIAS FOR $4;
1721         proceed BOOLEAN;
1722         org INTEGER;
1723         org_list INTEGER[];
1724         o INTEGER;
1725         sc TEXT;
1726         sce TEXT;
1727     BEGIN
1728         SELECT 'desired_sc' || field_suffix INTO sc;
1729         SELECT 'desired_sce' || field_suffix INTO sce;
1730         EXECUTE 'SELECT EXISTS (
1731             SELECT 1
1732             FROM information_schema.columns
1733             WHERE table_schema = $1
1734             AND table_name = $2
1735             and column_name = $3
1736         )' INTO proceed USING table_schema, table_name, sc;
1737         IF NOT proceed THEN
1738             RAISE EXCEPTION 'Missing column %', sc; 
1739         END IF;
1740         EXECUTE 'SELECT EXISTS (
1741             SELECT 1
1742             FROM information_schema.columns
1743             WHERE table_schema = $1
1744             AND table_name = $2
1745             and column_name = $3
1746         )' INTO proceed USING table_schema, table_name, sce;
1747         IF NOT proceed THEN
1748             RAISE EXCEPTION 'Missing column %', sce; 
1749         END IF;
1750
1751         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1752         IF org IS NULL THEN
1753             RAISE EXCEPTION 'Cannot find org by shortname';
1754         END IF;
1755
1756         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1757
1758         EXECUTE 'ALTER TABLE '
1759             || quote_ident(table_name)
1760             || ' DROP COLUMN IF EXISTS x_sc' || field_suffix;
1761         EXECUTE 'ALTER TABLE '
1762             || quote_ident(table_name)
1763             || ' ADD COLUMN x_sc' || field_suffix || ' INTEGER';
1764         EXECUTE 'ALTER TABLE '
1765             || quote_ident(table_name)
1766             || ' DROP COLUMN IF EXISTS x_sce' || field_suffix;
1767         EXECUTE 'ALTER TABLE '
1768             || quote_ident(table_name)
1769             || ' ADD COLUMN x_sce' || field_suffix || ' INTEGER';
1770
1771
1772         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1773             SET
1774                 x_sc' || field_suffix || ' = id
1775             FROM
1776                 (SELECT id, name, owner FROM asset_stat_cat
1777                     UNION SELECT id, name, owner FROM asset.stat_cat) u
1778             WHERE
1779                     BTRIM(UPPER(u.name)) = BTRIM(UPPER(' || sc || '))
1780                 AND u.owner = ANY ($1);'
1781         USING org_list;
1782
1783         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1784             SET
1785                 x_sce' || field_suffix || ' = id
1786             FROM
1787                 (SELECT id, stat_cat, owner, value FROM asset_stat_cat_entry
1788                     UNION SELECT id, stat_cat, owner, value FROM asset.stat_cat_entry) u
1789             WHERE
1790                     u.stat_cat = x_sc' || field_suffix || '
1791                 AND BTRIM(UPPER(u.value)) = BTRIM(UPPER(' || sce || '))
1792                 AND u.owner = ANY ($1);'
1793         USING org_list;
1794
1795         EXECUTE 'SELECT migration_tools.assert(
1796             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sc' || field_suffix || ' <> '''' AND x_sc' || field_suffix || ' IS NULL),
1797             ''Cannot find a desired stat cat'',
1798             ''Found all desired stat cats''
1799         );';
1800
1801         EXECUTE 'SELECT migration_tools.assert(
1802             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sce' || field_suffix || ' <> '''' AND x_sce' || field_suffix || ' IS NULL),
1803             ''Cannot find a desired stat cat entry'',
1804             ''Found all desired stat cat entries''
1805         );';
1806
1807     END;
1808 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1809
1810 -- convenience function for handling item barcode collisions in asset_copy_legacy
1811
1812 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
1813 DECLARE
1814     x_barcode TEXT;
1815     x_id BIGINT;
1816     row_count NUMERIC;
1817     internal_collision_count NUMERIC := 0;
1818     incumbent_collision_count NUMERIC := 0;
1819 BEGIN
1820     FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1821     LOOP
1822         FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
1823         LOOP
1824             UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
1825             GET DIAGNOSTICS row_count = ROW_COUNT;
1826             internal_collision_count := internal_collision_count + row_count;
1827         END LOOP;
1828     END LOOP;
1829     RAISE INFO '% internal collisions', internal_collision_count;
1830     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
1831     LOOP
1832         FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
1833         LOOP
1834             UPDATE asset_copy_legacy SET barcode = migration_schema || '_incumbent_collision_' || id || '_' || barcode WHERE id = x_id;
1835             GET DIAGNOSTICS row_count = ROW_COUNT;
1836             incumbent_collision_count := incumbent_collision_count + row_count;
1837         END LOOP;
1838     END LOOP;
1839     RAISE INFO '% incumbent collisions', incumbent_collision_count;
1840 END
1841 $function$ LANGUAGE plpgsql;
1842
1843 -- convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
1844 -- this should be ran prior to populating actor_card
1845
1846 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
1847 DECLARE
1848     x_barcode TEXT;
1849     x_id BIGINT;
1850     row_count NUMERIC;
1851     internal_collision_count NUMERIC := 0;
1852     incumbent_barcode_collision_count NUMERIC := 0;
1853     incumbent_usrname_collision_count NUMERIC := 0;
1854 BEGIN
1855     FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1856     LOOP
1857         FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1858         LOOP
1859             UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
1860             GET DIAGNOSTICS row_count = ROW_COUNT;
1861             internal_collision_count := internal_collision_count + row_count;
1862         END LOOP;
1863     END LOOP;
1864     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
1865
1866     FOR x_barcode IN
1867         SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
1868     LOOP
1869         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1870         LOOP
1871             UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_barcode_collision_' || id || '_' || usrname WHERE id = x_id;
1872             GET DIAGNOSTICS row_count = ROW_COUNT;
1873             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
1874         END LOOP;
1875     END LOOP;
1876     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
1877
1878     FOR x_barcode IN
1879         SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
1880     LOOP
1881         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1882         LOOP
1883             UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_usrname_collision_' || id || '_' || usrname WHERE id = x_id;
1884             GET DIAGNOSTICS row_count = ROW_COUNT;
1885             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
1886         END LOOP;
1887     END LOOP;
1888     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
1889 END
1890 $function$ LANGUAGE plpgsql;
1891
1892 -- alternate version: convenience function for handling item barcode collisions in asset_copy_legacy
1893
1894 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
1895 DECLARE
1896     x_barcode TEXT;
1897     x_id BIGINT;
1898     row_count NUMERIC;
1899     internal_collision_count NUMERIC := 0;
1900     incumbent_collision_count NUMERIC := 0;
1901 BEGIN
1902     FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1903     LOOP
1904         FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
1905         LOOP
1906             UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
1907             GET DIAGNOSTICS row_count = ROW_COUNT;
1908             internal_collision_count := internal_collision_count + row_count;
1909         END LOOP;
1910     END LOOP;
1911     RAISE INFO '% internal collisions', internal_collision_count;
1912     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
1913     LOOP
1914         FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
1915         LOOP
1916             UPDATE asset_copy_legacy SET barcode = migration_schema || '_' || barcode WHERE id = x_id;
1917             GET DIAGNOSTICS row_count = ROW_COUNT;
1918             incumbent_collision_count := incumbent_collision_count + row_count;
1919         END LOOP;
1920     END LOOP;
1921     RAISE INFO '% incumbent collisions', incumbent_collision_count;
1922 END
1923 $function$ LANGUAGE plpgsql;
1924
1925 -- alternate version: convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
1926 -- this should be ran prior to populating actor_card
1927
1928 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
1929 DECLARE
1930     x_barcode TEXT;
1931     x_id BIGINT;
1932     row_count NUMERIC;
1933     internal_collision_count NUMERIC := 0;
1934     incumbent_barcode_collision_count NUMERIC := 0;
1935     incumbent_usrname_collision_count NUMERIC := 0;
1936 BEGIN
1937     FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1938     LOOP
1939         FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1940         LOOP
1941             UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
1942             GET DIAGNOSTICS row_count = ROW_COUNT;
1943             internal_collision_count := internal_collision_count + row_count;
1944         END LOOP;
1945     END LOOP;
1946     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
1947
1948     FOR x_barcode IN
1949         SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
1950     LOOP
1951         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1952         LOOP
1953             UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
1954             GET DIAGNOSTICS row_count = ROW_COUNT;
1955             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
1956         END LOOP;
1957     END LOOP;
1958     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
1959
1960     FOR x_barcode IN
1961         SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
1962     LOOP
1963         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1964         LOOP
1965             UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
1966             GET DIAGNOSTICS row_count = ROW_COUNT;
1967             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
1968         END LOOP;
1969     END LOOP;
1970     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
1971 END
1972 $function$ LANGUAGE plpgsql;
1973
1974 CREATE OR REPLACE FUNCTION migration_tools.is_circ_rule_safe_to_delete( test_matchpoint INTEGER ) RETURNS BOOLEAN AS $func$
1975 -- WARNING: Use at your own risk
1976 -- FIXME: not considering marc_type, marc_form, marc_bib_level, marc_vr_format, usr_age_lower_bound, usr_age_upper_bound, item_age
1977 DECLARE
1978     item_object asset.copy%ROWTYPE;
1979     user_object actor.usr%ROWTYPE;
1980     test_rule_object config.circ_matrix_matchpoint%ROWTYPE;
1981     result_rule_object config.circ_matrix_matchpoint%ROWTYPE;
1982     safe_to_delete BOOLEAN := FALSE;
1983     m action.found_circ_matrix_matchpoint;
1984     n action.found_circ_matrix_matchpoint;
1985     -- ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] )
1986     result_matchpoint INTEGER;
1987 BEGIN
1988     SELECT INTO test_rule_object * FROM config.circ_matrix_matchpoint WHERE id = test_matchpoint;
1989     RAISE INFO 'testing rule: %', test_rule_object;
1990
1991     INSERT INTO actor.usr (
1992         profile,
1993         usrname,
1994         passwd,
1995         ident_type,
1996         first_given_name,
1997         family_name,
1998         home_ou,
1999         juvenile
2000     ) SELECT
2001         COALESCE(test_rule_object.grp, 2),
2002         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
2003         MD5(NOW()::TEXT),
2004         1,
2005         'Ima',
2006         'Test',
2007         COALESCE(test_rule_object.user_home_ou, test_rule_object.org_unit),
2008         COALESCE(test_rule_object.juvenile_flag, FALSE)
2009     ;
2010     
2011     SELECT INTO user_object * FROM actor.usr WHERE id = currval('actor.usr_id_seq');
2012
2013     INSERT INTO asset.call_number (
2014         creator,
2015         editor,
2016         record,
2017         owning_lib,
2018         label,
2019         label_class
2020     ) SELECT
2021         1,
2022         1,
2023         -1,
2024         COALESCE(test_rule_object.copy_owning_lib,test_rule_object.org_unit),
2025         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
2026         1
2027     ;
2028
2029     INSERT INTO asset.copy (
2030         barcode,
2031         circ_lib,
2032         creator,
2033         call_number,
2034         editor,
2035         location,
2036         loan_duration,
2037         fine_level,
2038         ref,
2039         circ_modifier
2040     ) SELECT
2041         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
2042         COALESCE(test_rule_object.copy_circ_lib,test_rule_object.org_unit),
2043         1,
2044         currval('asset.call_number_id_seq'),
2045         1,
2046         COALESCE(test_rule_object.copy_location,1),
2047         2,
2048         2,
2049         COALESCE(test_rule_object.ref_flag,FALSE),
2050         test_rule_object.circ_modifier
2051     ;
2052
2053     SELECT INTO item_object * FROM asset.copy WHERE id = currval('asset.copy_id_seq');
2054
2055     SELECT INTO m * FROM action.find_circ_matrix_matchpoint(
2056         test_rule_object.org_unit,
2057         item_object,
2058         user_object,
2059         COALESCE(test_rule_object.is_renewal,FALSE)
2060     );
2061     RAISE INFO '   action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
2062         test_rule_object.org_unit,
2063         item_object.id,
2064         user_object.id,
2065         COALESCE(test_rule_object.is_renewal,FALSE),
2066         m.success,
2067         m.matchpoint,
2068         m.buildrows
2069     ;
2070
2071     --  disable the rule being tested to see if the outcome changes
2072     UPDATE config.circ_matrix_matchpoint SET active = FALSE WHERE id = (m.matchpoint).id;
2073
2074     SELECT INTO n * FROM action.find_circ_matrix_matchpoint(
2075         test_rule_object.org_unit,
2076         item_object,
2077         user_object,
2078         COALESCE(test_rule_object.is_renewal,FALSE)
2079     );
2080     RAISE INFO 'VS action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
2081         test_rule_object.org_unit,
2082         item_object.id,
2083         user_object.id,
2084         COALESCE(test_rule_object.is_renewal,FALSE),
2085         n.success,
2086         n.matchpoint,
2087         n.buildrows
2088     ;
2089
2090     -- FIXME: We could dig deeper and see if the referenced config.rule_*
2091     -- entries are effectively equivalent, but for now, let's assume no
2092     -- duplicate rules at that level
2093     IF (
2094             (m.matchpoint).circulate = (n.matchpoint).circulate
2095         AND (m.matchpoint).duration_rule = (n.matchpoint).duration_rule
2096         AND (m.matchpoint).recurring_fine_rule = (n.matchpoint).recurring_fine_rule
2097         AND (m.matchpoint).max_fine_rule = (n.matchpoint).max_fine_rule
2098         AND (
2099                 (m.matchpoint).hard_due_date = (n.matchpoint).hard_due_date
2100                 OR (
2101                         (m.matchpoint).hard_due_date IS NULL
2102                     AND (n.matchpoint).hard_due_date IS NULL
2103                 )
2104         )
2105         AND (
2106                 (m.matchpoint).renewals = (n.matchpoint).renewals
2107                 OR (
2108                         (m.matchpoint).renewals IS NULL
2109                     AND (n.matchpoint).renewals IS NULL
2110                 )
2111         )
2112         AND (
2113                 (m.matchpoint).grace_period = (n.matchpoint).grace_period
2114                 OR (
2115                         (m.matchpoint).grace_period IS NULL
2116                     AND (n.matchpoint).grace_period IS NULL
2117                 )
2118         )
2119         AND (
2120                 (m.matchpoint).total_copy_hold_ratio = (n.matchpoint).total_copy_hold_ratio
2121                 OR (
2122                         (m.matchpoint).total_copy_hold_ratio IS NULL
2123                     AND (n.matchpoint).total_copy_hold_ratio IS NULL
2124                 )
2125         )
2126         AND (
2127                 (m.matchpoint).available_copy_hold_ratio = (n.matchpoint).available_copy_hold_ratio
2128                 OR (
2129                         (m.matchpoint).available_copy_hold_ratio IS NULL
2130                     AND (n.matchpoint).available_copy_hold_ratio IS NULL
2131                 )
2132         )
2133         AND NOT EXISTS (
2134             SELECT limit_set, fallthrough
2135             FROM config.circ_matrix_limit_set_map
2136             WHERE active and matchpoint = (m.matchpoint).id
2137             EXCEPT
2138             SELECT limit_set, fallthrough
2139             FROM config.circ_matrix_limit_set_map
2140             WHERE active and matchpoint = (n.matchpoint).id
2141         )
2142
2143     ) THEN
2144         RAISE INFO 'rule has same outcome';
2145         safe_to_delete := TRUE;
2146     ELSE
2147         RAISE INFO 'rule has different outcome';
2148         safe_to_delete := FALSE;
2149     END IF;
2150
2151     RAISE EXCEPTION 'rollback the temporary changes';
2152
2153 EXCEPTION WHEN OTHERS THEN
2154
2155     RAISE INFO 'inside exception block: %, %', SQLSTATE, SQLERRM;
2156     RETURN safe_to_delete;
2157
2158 END;
2159 $func$ LANGUAGE plpgsql;
2160