for convenience, add some other values to handle_not_migrate
[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.m_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.m_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.m_actor_usr_legacy and have 'usrname' assigned.
360 --        Then SELECT migration_tools.create_cards('m_foo');
361
362 DECLARE
363         u                    TEXT := schemaname || '.m_actor_usr_legacy';
364         c                    TEXT := schemaname || '.m_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 || $$/m_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 || $$/m_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 || $$/m_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 || $$/m_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 || $$/m_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 || $$/m_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 || $$/m_asset_call_number_suffix'$$;
483    EXECUTE $$COPY config.rule_circ_duration TO '$$ ||  dir || $$/m_config_rule_circ_duration'$$;
484    EXECUTE $$COPY config.rule_age_hold_protect TO '$$ ||  dir || $$/m_config_rule_age_hold_protect'$$;
485    EXECUTE $$COPY config.rule_max_fine TO '$$ ||  dir || $$/m_config_rule_max_fine'$$;
486    EXECUTE $$COPY config.rule_recurring_fine TO '$$ ||  dir || $$/m_config_rule_recurring_fine'$$;
487    EXECUTE $$COPY permission.grp_tree TO '$$ ||  dir || $$/m_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 || $$/m_actor_hours_of_operation'$$;
494    EXECUTE $$COPY actor.org_unit_closed (org_unit, close_start, close_end, reason) FROM '$$ ||  dir || $$/m_actor_org_unit_closed'$$;
495    EXECUTE $$COPY actor.org_unit_setting (org_unit, name, value) FROM '$$ ||  dir || $$/m_actor_org_unit_setting'$$;
496    EXECUTE $$COPY asset.copy_location (name, owning_lib, holdable, hold_verify, opac_visible, circulate) FROM '$$ ||  dir || $$/m_asset_copy_location'$$;
497    EXECUTE $$COPY permission.grp_penalty_threshold (grp, org_unit, penalty, threshold) FROM '$$ ||  dir || $$/m_permission_grp_penalty_threshold'$$;
498    EXECUTE $$COPY asset.call_number_prefix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/m_asset_call_number_prefix'$$;
499    EXECUTE $$COPY asset.call_number_suffix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/m_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 m_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 m_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 m_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) = ''Delete'' THEN FALSE'
1109             || ' WHEN BTRIM(desired_not_migrate) = ''Do Not Migrate'' THEN FALSE'
1110             || ' WHEN BTRIM(desired_not_migrate) = ''FALSE'' THEN TRUE'
1111             || ' WHEN BTRIM(desired_not_migrate) = ''Migrate'' THEN TRUE'
1112             || ' WHEN BTRIM(desired_not_migrate) = ''Keep'' THEN TRUE'
1113             || ' WHEN BTRIM(desired_not_migrate) = '''' THEN TRUE'
1114             || ' END';
1115
1116         EXECUTE 'SELECT migration_tools.assert(
1117             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE x_migrate IS NULL),
1118             ''Not all desired_not_migrate values understood'',
1119             ''All desired_not_migrate values understood''
1120         );';
1121
1122     END;
1123 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1124
1125 -- convenience function for handling desired_not_migrate
1126
1127 CREATE OR REPLACE FUNCTION migration_tools.handle_barred_or_blocked (TEXT,TEXT) RETURNS VOID AS $$
1128     DECLARE
1129         table_schema ALIAS FOR $1;
1130         table_name ALIAS FOR $2;
1131         proceed BOOLEAN;
1132     BEGIN
1133         EXECUTE 'SELECT EXISTS (
1134             SELECT 1
1135             FROM information_schema.columns
1136             WHERE table_schema = $1
1137             AND table_name = $2
1138             and column_name = ''desired_barred_or_blocked''
1139         )' INTO proceed USING table_schema, table_name;
1140         IF NOT proceed THEN
1141             RAISE EXCEPTION 'Missing column desired_barred_or_blocked'; 
1142         END IF;
1143
1144         EXECUTE 'ALTER TABLE '
1145             || quote_ident(table_name)
1146             || ' DROP COLUMN IF EXISTS x_barred';
1147         EXECUTE 'ALTER TABLE '
1148             || quote_ident(table_name)
1149             || ' ADD COLUMN x_barred BOOLEAN';
1150
1151         EXECUTE 'ALTER TABLE '
1152             || quote_ident(table_name)
1153             || ' DROP COLUMN IF EXISTS x_blocked';
1154         EXECUTE 'ALTER TABLE '
1155             || quote_ident(table_name)
1156             || ' ADD COLUMN x_blocked BOOLEAN';
1157
1158         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1159             || ' SET x_barred = CASE'
1160             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Barred'' THEN TRUE'
1161             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Blocked'' THEN FALSE'
1162             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Neither'' THEN FALSE'
1163             || ' WHEN BTRIM(desired_barred_or_blocked) = '''' THEN FALSE'
1164             || ' END';
1165
1166         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1167             || ' SET x_blocked = CASE'
1168             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Blocked'' THEN TRUE'
1169             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Barred'' THEN FALSE'
1170             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Neither'' THEN FALSE'
1171             || ' WHEN BTRIM(desired_barred_or_blocked) = '''' THEN FALSE'
1172             || ' END';
1173
1174         EXECUTE 'SELECT migration_tools.assert(
1175             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE x_barred IS NULL or x_blocked IS NULL),
1176             ''Not all desired_barred_or_blocked values understood'',
1177             ''All desired_barred_or_blocked values understood''
1178         );';
1179
1180     END;
1181 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1182
1183 -- convenience function for handling desired_profile
1184
1185 CREATE OR REPLACE FUNCTION migration_tools.handle_profile (TEXT,TEXT) RETURNS VOID AS $$
1186     DECLARE
1187         table_schema ALIAS FOR $1;
1188         table_name ALIAS FOR $2;
1189         proceed BOOLEAN;
1190     BEGIN
1191         EXECUTE 'SELECT EXISTS (
1192             SELECT 1
1193             FROM information_schema.columns
1194             WHERE table_schema = $1
1195             AND table_name = $2
1196             and column_name = ''desired_profile''
1197         )' INTO proceed USING table_schema, table_name;
1198         IF NOT proceed THEN
1199             RAISE EXCEPTION 'Missing column desired_profile'; 
1200         END IF;
1201
1202         EXECUTE 'ALTER TABLE '
1203             || quote_ident(table_name)
1204             || ' DROP COLUMN IF EXISTS x_profile';
1205         EXECUTE 'ALTER TABLE '
1206             || quote_ident(table_name)
1207             || ' ADD COLUMN x_profile INTEGER';
1208
1209         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1210             || ' SET x_profile = b.id FROM permission.grp_tree b'
1211             || ' WHERE BTRIM(UPPER(a.desired_profile)) = BTRIM(UPPER(b.name))';
1212
1213         EXECUTE 'SELECT migration_tools.assert(
1214             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_profile <> '''' AND x_profile IS NULL),
1215             ''Cannot find a desired profile'',
1216             ''Found all desired profiles''
1217         );';
1218
1219     END;
1220 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1221
1222 -- convenience function for handling desired actor stat cats
1223
1224 CREATE OR REPLACE FUNCTION migration_tools.vivicate_actor_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1225     DECLARE
1226         table_schema ALIAS FOR $1;
1227         table_name ALIAS FOR $2;
1228         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1229         org_shortname ALIAS FOR $4;
1230         proceed BOOLEAN;
1231         org INTEGER;
1232         org_list INTEGER[];
1233         sc TEXT;
1234         sce TEXT;
1235     BEGIN
1236
1237         SELECT 'desired_sc' || field_suffix INTO sc;
1238         SELECT 'desired_sce' || field_suffix INTO sce;
1239
1240         EXECUTE 'SELECT EXISTS (
1241             SELECT 1
1242             FROM information_schema.columns
1243             WHERE table_schema = $1
1244             AND table_name = $2
1245             and column_name = $3
1246         )' INTO proceed USING table_schema, table_name, sc;
1247         IF NOT proceed THEN
1248             RAISE EXCEPTION 'Missing column %', sc; 
1249         END IF;
1250         EXECUTE 'SELECT EXISTS (
1251             SELECT 1
1252             FROM information_schema.columns
1253             WHERE table_schema = $1
1254             AND table_name = $2
1255             and column_name = $3
1256         )' INTO proceed USING table_schema, table_name, sce;
1257         IF NOT proceed THEN
1258             RAISE EXCEPTION 'Missing column %', sce; 
1259         END IF;
1260
1261         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1262         IF org IS NULL THEN
1263             RAISE EXCEPTION 'Cannot find org by shortname';
1264         END IF;
1265         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1266
1267         -- caller responsible for their own truncates though we try to prevent duplicates
1268         EXECUTE 'INSERT INTO m_actor_stat_cat (owner, name)
1269             SELECT DISTINCT
1270                  $1
1271                 ,BTRIM('||sc||')
1272             FROM 
1273                 ' || quote_ident(table_name) || '
1274             WHERE
1275                 NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1276                 AND NOT EXISTS (
1277                     SELECT id
1278                     FROM actor.stat_cat
1279                     WHERE owner = ANY ($2)
1280                     AND name = BTRIM('||sc||')
1281                 )
1282                 AND NOT EXISTS (
1283                     SELECT id
1284                     FROM m_actor_stat_cat
1285                     WHERE owner = ANY ($2)
1286                     AND name = BTRIM('||sc||')
1287                 )
1288             ORDER BY 2;'
1289         USING org, org_list;
1290
1291         EXECUTE 'INSERT INTO m_actor_stat_cat_entry (stat_cat, owner, value)
1292             SELECT DISTINCT
1293                 COALESCE(
1294                     (SELECT id
1295                         FROM actor.stat_cat
1296                         WHERE owner = ANY ($2)
1297                         AND BTRIM('||sc||') = BTRIM(name))
1298                    ,(SELECT id
1299                         FROM m_actor_stat_cat
1300                         WHERE owner = ANY ($2)
1301                         AND BTRIM('||sc||') = BTRIM(name))
1302                 )
1303                 ,$1
1304                 ,BTRIM('||sce||')
1305             FROM 
1306                 ' || quote_ident(table_name) || '
1307             WHERE
1308                     NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1309                 AND NULLIF(BTRIM('||sce||'),'''') IS NOT NULL
1310                 AND NOT EXISTS (
1311                     SELECT id
1312                     FROM actor.stat_cat_entry
1313                     WHERE stat_cat = (
1314                         SELECT id
1315                         FROM actor.stat_cat
1316                         WHERE owner = ANY ($2)
1317                         AND BTRIM('||sc||') = BTRIM(name)
1318                     ) AND value = BTRIM('||sce||')
1319                     AND owner = ANY ($2)
1320                 )
1321                 AND NOT EXISTS (
1322                     SELECT id
1323                     FROM m_actor_stat_cat_entry
1324                     WHERE stat_cat = (
1325                         SELECT id
1326                         FROM m_actor_stat_cat
1327                         WHERE owner = ANY ($2)
1328                         AND BTRIM('||sc||') = BTRIM(name)
1329                     ) AND value = BTRIM('||sce||')
1330                     AND owner = ANY ($2)
1331                 )
1332             ORDER BY 1,3;'
1333         USING org, org_list;
1334     END;
1335 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1336
1337 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1338     DECLARE
1339         table_schema ALIAS FOR $1;
1340         table_name ALIAS FOR $2;
1341         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1342         org_shortname ALIAS FOR $4;
1343         proceed BOOLEAN;
1344         org INTEGER;
1345         org_list INTEGER[];
1346         o INTEGER;
1347         sc TEXT;
1348         sce TEXT;
1349     BEGIN
1350         SELECT 'desired_sc' || field_suffix INTO sc;
1351         SELECT 'desired_sce' || field_suffix INTO sce;
1352         EXECUTE 'SELECT EXISTS (
1353             SELECT 1
1354             FROM information_schema.columns
1355             WHERE table_schema = $1
1356             AND table_name = $2
1357             and column_name = $3
1358         )' INTO proceed USING table_schema, table_name, sc;
1359         IF NOT proceed THEN
1360             RAISE EXCEPTION 'Missing column %', sc; 
1361         END IF;
1362         EXECUTE 'SELECT EXISTS (
1363             SELECT 1
1364             FROM information_schema.columns
1365             WHERE table_schema = $1
1366             AND table_name = $2
1367             and column_name = $3
1368         )' INTO proceed USING table_schema, table_name, sce;
1369         IF NOT proceed THEN
1370             RAISE EXCEPTION 'Missing column %', sce; 
1371         END IF;
1372
1373         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1374         IF org IS NULL THEN
1375             RAISE EXCEPTION 'Cannot find org by shortname';
1376         END IF;
1377
1378         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1379
1380         EXECUTE 'ALTER TABLE '
1381             || quote_ident(table_name)
1382             || ' DROP COLUMN IF EXISTS x_sc' || field_suffix;
1383         EXECUTE 'ALTER TABLE '
1384             || quote_ident(table_name)
1385             || ' ADD COLUMN x_sc' || field_suffix || ' INTEGER';
1386         EXECUTE 'ALTER TABLE '
1387             || quote_ident(table_name)
1388             || ' DROP COLUMN IF EXISTS x_sce' || field_suffix;
1389         EXECUTE 'ALTER TABLE '
1390             || quote_ident(table_name)
1391             || ' ADD COLUMN x_sce' || field_suffix || ' INTEGER';
1392
1393
1394         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1395             SET
1396                 x_sc' || field_suffix || ' = id
1397             FROM
1398                 (SELECT id, name, owner FROM m_actor_stat_cat
1399                     UNION SELECT id, name, owner FROM actor.stat_cat) u
1400             WHERE
1401                     BTRIM(UPPER(u.name)) = BTRIM(UPPER(' || sc || '))
1402                 AND u.owner = ANY ($1);'
1403         USING org_list;
1404
1405         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1406             SET
1407                 x_sce' || field_suffix || ' = id
1408             FROM
1409                 (SELECT id, stat_cat, owner, value FROM m_actor_stat_cat_entry
1410                     UNION SELECT id, stat_cat, owner, value FROM actor.stat_cat_entry) u
1411             WHERE
1412                     u.stat_cat = x_sc' || field_suffix || '
1413                 AND BTRIM(UPPER(u.value)) = BTRIM(UPPER(' || sce || '))
1414                 AND u.owner = ANY ($1);'
1415         USING org_list;
1416
1417         EXECUTE 'SELECT migration_tools.assert(
1418             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sc' || field_suffix || ' <> '''' AND x_sc' || field_suffix || ' IS NULL),
1419             ''Cannot find a desired stat cat'',
1420             ''Found all desired stat cats''
1421         );';
1422
1423         EXECUTE 'SELECT migration_tools.assert(
1424             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sce' || field_suffix || ' <> '''' AND x_sce' || field_suffix || ' IS NULL),
1425             ''Cannot find a desired stat cat entry'',
1426             ''Found all desired stat cat entries''
1427         );';
1428
1429     END;
1430 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1431
1432 -- convenience functions for adding shelving locations
1433 DROP FUNCTION IF EXISTS migration_tools.find_shelf(INT,TEXT);
1434 CREATE OR REPLACE FUNCTION migration_tools.find_shelf(org_id INT, shelf_name TEXT) RETURNS INTEGER AS $$
1435 DECLARE
1436     return_id   INT;
1437     d           INT;
1438     cur_id      INT;
1439 BEGIN
1440     SELECT INTO d MAX(distance) FROM actor.org_unit_ancestors_distance(org_id);
1441     WHILE d >= 0
1442     LOOP
1443         SELECT INTO cur_id id FROM actor.org_unit_ancestor_at_depth(org_id,d);
1444         SELECT INTO return_id id FROM asset.copy_location WHERE owning_lib = cur_id AND name ILIKE shelf_name;
1445         IF return_id IS NOT NULL THEN
1446                 RETURN return_id;
1447         END IF;
1448         d := d - 1;
1449     END LOOP;
1450
1451     RETURN NULL;
1452 END
1453 $$ LANGUAGE plpgsql;
1454
1455 -- may remove later but testing using this with new migration scripts and not loading acls until go live
1456
1457 DROP FUNCTION IF EXISTS migration_tools.find_mig_shelf(INT,TEXT);
1458 CREATE OR REPLACE FUNCTION migration_tools.find_mig_shelf(org_id INT, shelf_name TEXT) RETURNS INTEGER AS $$
1459 DECLARE
1460     return_id   INT;
1461     d           INT;
1462     cur_id      INT;
1463 BEGIN
1464     SELECT INTO d MAX(distance) FROM actor.org_unit_ancestors_distance(org_id);
1465     WHILE d >= 0
1466     LOOP
1467         SELECT INTO cur_id id FROM actor.org_unit_ancestor_at_depth(org_id,d);
1468         
1469         SELECT INTO return_id id FROM 
1470             (SELECT * FROM asset.copy_location UNION ALL SELECT * FROM m_asset_copy_location) x
1471             WHERE owning_lib = cur_id AND name ILIKE shelf_name;
1472         IF return_id IS NOT NULL THEN
1473                 RETURN return_id;
1474         END IF;
1475         d := d - 1;
1476     END LOOP;
1477
1478     RETURN NULL;
1479 END
1480 $$ LANGUAGE plpgsql;
1481
1482 -- convenience function for linking to the item staging table
1483
1484 CREATE OR REPLACE FUNCTION migration_tools.handle_item_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
1485     DECLARE
1486         table_schema ALIAS FOR $1;
1487         table_name ALIAS FOR $2;
1488         foreign_column_name ALIAS FOR $3;
1489         main_column_name ALIAS FOR $4;
1490         btrim_desired ALIAS FOR $5;
1491         proceed BOOLEAN;
1492     BEGIN
1493         EXECUTE 'SELECT EXISTS (
1494             SELECT 1
1495             FROM information_schema.columns
1496             WHERE table_schema = $1
1497             AND table_name = $2
1498             and column_name = $3
1499         )' INTO proceed USING table_schema, table_name, foreign_column_name;
1500         IF NOT proceed THEN
1501             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
1502         END IF;
1503
1504         EXECUTE 'SELECT EXISTS (
1505             SELECT 1
1506             FROM information_schema.columns
1507             WHERE table_schema = $1
1508             AND table_name = ''m_asset_copy_legacy''
1509             and column_name = $2
1510         )' INTO proceed USING table_schema, main_column_name;
1511         IF NOT proceed THEN
1512             RAISE EXCEPTION 'No %.m_asset_copy_legacy with column %', table_schema, main_column_name; 
1513         END IF;
1514
1515         EXECUTE 'ALTER TABLE '
1516             || quote_ident(table_name)
1517             || ' DROP COLUMN IF EXISTS x_item';
1518         EXECUTE 'ALTER TABLE '
1519             || quote_ident(table_name)
1520             || ' ADD COLUMN x_item BIGINT';
1521
1522         IF btrim_desired THEN
1523             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1524                 || ' SET x_item = b.id FROM m_asset_copy_legacy b'
1525                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
1526                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
1527         ELSE
1528             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1529                 || ' SET x_item = b.id FROM m_asset_copy_legacy b'
1530                 || ' WHERE a.' || quote_ident(foreign_column_name)
1531                 || ' = b.' || quote_ident(main_column_name);
1532         END IF;
1533
1534         --EXECUTE 'SELECT migration_tools.assert(
1535         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_item IS NULL),
1536         --    ''Cannot link every barcode'',
1537         --    ''Every barcode linked''
1538         --);';
1539
1540     END;
1541 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1542
1543 -- convenience function for linking to the user staging table
1544
1545 CREATE OR REPLACE FUNCTION migration_tools.handle_user_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
1546     DECLARE
1547         table_schema ALIAS FOR $1;
1548         table_name ALIAS FOR $2;
1549         foreign_column_name ALIAS FOR $3;
1550         main_column_name ALIAS FOR $4;
1551         btrim_desired ALIAS FOR $5;
1552         proceed BOOLEAN;
1553     BEGIN
1554         EXECUTE 'SELECT EXISTS (
1555             SELECT 1
1556             FROM information_schema.columns
1557             WHERE table_schema = $1
1558             AND table_name = $2
1559             and column_name = $3
1560         )' INTO proceed USING table_schema, table_name, foreign_column_name;
1561         IF NOT proceed THEN
1562             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
1563         END IF;
1564
1565         EXECUTE 'SELECT EXISTS (
1566             SELECT 1
1567             FROM information_schema.columns
1568             WHERE table_schema = $1
1569             AND table_name = ''m_actor_usr_legacy''
1570             and column_name = $2
1571         )' INTO proceed USING table_schema, main_column_name;
1572         IF NOT proceed THEN
1573             RAISE EXCEPTION 'No %.m_actor_usr_legacy with column %', table_schema, main_column_name; 
1574         END IF;
1575
1576         EXECUTE 'ALTER TABLE '
1577             || quote_ident(table_name)
1578             || ' DROP COLUMN IF EXISTS x_user';
1579         EXECUTE 'ALTER TABLE '
1580             || quote_ident(table_name)
1581             || ' ADD COLUMN x_user INTEGER';
1582
1583         IF btrim_desired THEN
1584             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1585                 || ' SET x_user = b.id FROM m_actor_usr_legacy b'
1586                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
1587                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
1588         ELSE
1589             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
1590                 || ' SET x_user = b.id FROM m_actor_usr_legacy b'
1591                 || ' WHERE a.' || quote_ident(foreign_column_name)
1592                 || ' = b.' || quote_ident(main_column_name);
1593         END IF;
1594
1595         --EXECUTE 'SELECT migration_tools.assert(
1596         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_user IS NULL),
1597         --    ''Cannot link every barcode'',
1598         --    ''Every barcode linked''
1599         --);';
1600
1601     END;
1602 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1603
1604 -- convenience function for handling desired asset stat cats
1605
1606 CREATE OR REPLACE FUNCTION migration_tools.vivicate_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1607     DECLARE
1608         table_schema ALIAS FOR $1;
1609         table_name ALIAS FOR $2;
1610         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1611         org_shortname ALIAS FOR $4;
1612         proceed BOOLEAN;
1613         org INTEGER;
1614         org_list INTEGER[];
1615         sc TEXT;
1616         sce TEXT;
1617     BEGIN
1618
1619         SELECT 'desired_sc' || field_suffix INTO sc;
1620         SELECT 'desired_sce' || field_suffix INTO sce;
1621
1622         EXECUTE 'SELECT EXISTS (
1623             SELECT 1
1624             FROM information_schema.columns
1625             WHERE table_schema = $1
1626             AND table_name = $2
1627             and column_name = $3
1628         )' INTO proceed USING table_schema, table_name, sc;
1629         IF NOT proceed THEN
1630             RAISE EXCEPTION 'Missing column %', sc; 
1631         END IF;
1632         EXECUTE 'SELECT EXISTS (
1633             SELECT 1
1634             FROM information_schema.columns
1635             WHERE table_schema = $1
1636             AND table_name = $2
1637             and column_name = $3
1638         )' INTO proceed USING table_schema, table_name, sce;
1639         IF NOT proceed THEN
1640             RAISE EXCEPTION 'Missing column %', sce; 
1641         END IF;
1642
1643         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1644         IF org IS NULL THEN
1645             RAISE EXCEPTION 'Cannot find org by shortname';
1646         END IF;
1647         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1648
1649         -- caller responsible for their own truncates though we try to prevent duplicates
1650         EXECUTE 'INSERT INTO m_asset_stat_cat (owner, name)
1651             SELECT DISTINCT
1652                  $1
1653                 ,BTRIM('||sc||')
1654             FROM 
1655                 ' || quote_ident(table_name) || '
1656             WHERE
1657                 NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1658                 AND NOT EXISTS (
1659                     SELECT id
1660                     FROM asset.stat_cat
1661                     WHERE owner = ANY ($2)
1662                     AND name = BTRIM('||sc||')
1663                 )
1664                 AND NOT EXISTS (
1665                     SELECT id
1666                     FROM m_asset_stat_cat
1667                     WHERE owner = ANY ($2)
1668                     AND name = BTRIM('||sc||')
1669                 )
1670             ORDER BY 2;'
1671         USING org, org_list;
1672
1673         EXECUTE 'INSERT INTO m_asset_stat_cat_entry (stat_cat, owner, value)
1674             SELECT DISTINCT
1675                 COALESCE(
1676                     (SELECT id
1677                         FROM asset.stat_cat
1678                         WHERE owner = ANY ($2)
1679                         AND BTRIM('||sc||') = BTRIM(name))
1680                    ,(SELECT id
1681                         FROM m_asset_stat_cat
1682                         WHERE owner = ANY ($2)
1683                         AND BTRIM('||sc||') = BTRIM(name))
1684                 )
1685                 ,$1
1686                 ,BTRIM('||sce||')
1687             FROM 
1688                 ' || quote_ident(table_name) || '
1689             WHERE
1690                     NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
1691                 AND NULLIF(BTRIM('||sce||'),'''') IS NOT NULL
1692                 AND NOT EXISTS (
1693                     SELECT id
1694                     FROM asset.stat_cat_entry
1695                     WHERE stat_cat = (
1696                         SELECT id
1697                         FROM asset.stat_cat
1698                         WHERE owner = ANY ($2)
1699                         AND BTRIM('||sc||') = BTRIM(name)
1700                     ) AND value = BTRIM('||sce||')
1701                     AND owner = ANY ($2)
1702                 )
1703                 AND NOT EXISTS (
1704                     SELECT id
1705                     FROM m_asset_stat_cat_entry
1706                     WHERE stat_cat = (
1707                         SELECT id
1708                         FROM m_asset_stat_cat
1709                         WHERE owner = ANY ($2)
1710                         AND BTRIM('||sc||') = BTRIM(name)
1711                     ) AND value = BTRIM('||sce||')
1712                     AND owner = ANY ($2)
1713                 )
1714             ORDER BY 1,3;'
1715         USING org, org_list;
1716     END;
1717 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1718
1719 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1720     DECLARE
1721         table_schema ALIAS FOR $1;
1722         table_name ALIAS FOR $2;
1723         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
1724         org_shortname ALIAS FOR $4;
1725         proceed BOOLEAN;
1726         org INTEGER;
1727         org_list INTEGER[];
1728         o INTEGER;
1729         sc TEXT;
1730         sce TEXT;
1731     BEGIN
1732         SELECT 'desired_sc' || field_suffix INTO sc;
1733         SELECT 'desired_sce' || field_suffix INTO sce;
1734         EXECUTE 'SELECT EXISTS (
1735             SELECT 1
1736             FROM information_schema.columns
1737             WHERE table_schema = $1
1738             AND table_name = $2
1739             and column_name = $3
1740         )' INTO proceed USING table_schema, table_name, sc;
1741         IF NOT proceed THEN
1742             RAISE EXCEPTION 'Missing column %', sc; 
1743         END IF;
1744         EXECUTE 'SELECT EXISTS (
1745             SELECT 1
1746             FROM information_schema.columns
1747             WHERE table_schema = $1
1748             AND table_name = $2
1749             and column_name = $3
1750         )' INTO proceed USING table_schema, table_name, sce;
1751         IF NOT proceed THEN
1752             RAISE EXCEPTION 'Missing column %', sce; 
1753         END IF;
1754
1755         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
1756         IF org IS NULL THEN
1757             RAISE EXCEPTION 'Cannot find org by shortname';
1758         END IF;
1759
1760         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
1761
1762         EXECUTE 'ALTER TABLE '
1763             || quote_ident(table_name)
1764             || ' DROP COLUMN IF EXISTS x_sc' || field_suffix;
1765         EXECUTE 'ALTER TABLE '
1766             || quote_ident(table_name)
1767             || ' ADD COLUMN x_sc' || field_suffix || ' INTEGER';
1768         EXECUTE 'ALTER TABLE '
1769             || quote_ident(table_name)
1770             || ' DROP COLUMN IF EXISTS x_sce' || field_suffix;
1771         EXECUTE 'ALTER TABLE '
1772             || quote_ident(table_name)
1773             || ' ADD COLUMN x_sce' || field_suffix || ' INTEGER';
1774
1775
1776         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1777             SET
1778                 x_sc' || field_suffix || ' = id
1779             FROM
1780                 (SELECT id, name, owner FROM m_asset_stat_cat
1781                     UNION SELECT id, name, owner FROM asset.stat_cat) u
1782             WHERE
1783                     BTRIM(UPPER(u.name)) = BTRIM(UPPER(' || sc || '))
1784                 AND u.owner = ANY ($1);'
1785         USING org_list;
1786
1787         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
1788             SET
1789                 x_sce' || field_suffix || ' = id
1790             FROM
1791                 (SELECT id, stat_cat, owner, value FROM m_asset_stat_cat_entry
1792                     UNION SELECT id, stat_cat, owner, value FROM asset.stat_cat_entry) u
1793             WHERE
1794                     u.stat_cat = x_sc' || field_suffix || '
1795                 AND BTRIM(UPPER(u.value)) = BTRIM(UPPER(' || sce || '))
1796                 AND u.owner = ANY ($1);'
1797         USING org_list;
1798
1799         EXECUTE 'SELECT migration_tools.assert(
1800             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sc' || field_suffix || ' <> '''' AND x_sc' || field_suffix || ' IS NULL),
1801             ''Cannot find a desired stat cat'',
1802             ''Found all desired stat cats''
1803         );';
1804
1805         EXECUTE 'SELECT migration_tools.assert(
1806             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sce' || field_suffix || ' <> '''' AND x_sce' || field_suffix || ' IS NULL),
1807             ''Cannot find a desired stat cat entry'',
1808             ''Found all desired stat cat entries''
1809         );';
1810
1811     END;
1812 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1813
1814 -- convenience function for handling item barcode collisions in m_asset_copy_legacy
1815
1816 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
1817 DECLARE
1818     xxx_barcode TEXT;
1819     xxx_id BIGINT;
1820     row_count NUMERIC;
1821     internal_collision_count NUMERIC := 0;
1822     incumbent_collision_count NUMERIC := 0;
1823 BEGIN
1824     FOR xxx_barcode IN SELECT barcode FROM m_asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1825     LOOP
1826         FOR xxx_id IN SELECT id FROM m_asset_copy WHERE barcode = xxx_barcode
1827         LOOP
1828             UPDATE m_asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = xxx_id;
1829             GET DIAGNOSTICS row_count = ROW_COUNT;
1830             internal_collision_count := internal_collision_count + row_count;
1831         END LOOP;
1832     END LOOP;
1833     RAISE INFO '% internal collisions', internal_collision_count;
1834     FOR xxx_barcode IN SELECT a.barcode FROM asset.copy a, m_asset_copy_legacy b WHERE x_migrate AND a.deleted IS FALSE AND a.barcode = b.barcode
1835     LOOP
1836         FOR xxx_id IN SELECT id FROM m_asset_copy_legacy WHERE barcode = xxx_barcode
1837         LOOP
1838             UPDATE m_asset_copy_legacy SET barcode = migration_schema || '_incumbent_collision_' || id || '_' || barcode WHERE id = xxx_id;
1839             GET DIAGNOSTICS row_count = ROW_COUNT;
1840             incumbent_collision_count := incumbent_collision_count + row_count;
1841         END LOOP;
1842     END LOOP;
1843     RAISE INFO '% incumbent collisions', incumbent_collision_count;
1844 END
1845 $function$ LANGUAGE plpgsql;
1846
1847 -- convenience function for handling patron barcode/usrname collisions in m_actor_usr_legacy
1848 -- this should be ran prior to populating m_actor_card
1849
1850 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
1851 DECLARE
1852     xxx_barcode TEXT;
1853     xxx_id BIGINT;
1854     row_count NUMERIC;
1855     internal_collision_count NUMERIC := 0;
1856     incumbent_barcode_collision_count NUMERIC := 0;
1857     incumbent_usrname_collision_count NUMERIC := 0;
1858 BEGIN
1859     FOR xxx_barcode IN SELECT usrname FROM m_actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1860     LOOP
1861         FOR xxx_id IN SELECT id FROM m_actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
1862         LOOP
1863             UPDATE m_actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = xxx_id;
1864             GET DIAGNOSTICS row_count = ROW_COUNT;
1865             internal_collision_count := internal_collision_count + row_count;
1866         END LOOP;
1867     END LOOP;
1868     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
1869
1870     FOR xxx_barcode IN
1871         SELECT a.barcode FROM actor.card a, m_actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
1872     LOOP
1873         FOR xxx_id IN SELECT DISTINCT id FROM m_actor_usr_legacy WHERE x_migrate AND usrname = xxx_barcode
1874         LOOP
1875             UPDATE m_actor_usr_legacy SET usrname = migration_schema || '_incumbent_barcode_collision_' || id || '_' || usrname WHERE id = xxx_id;
1876             GET DIAGNOSTICS row_count = ROW_COUNT;
1877             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
1878         END LOOP;
1879     END LOOP;
1880     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
1881
1882     FOR xxx_barcode IN
1883         SELECT a.usrname FROM actor.usr a, m_actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
1884     LOOP
1885         FOR xxx_id IN SELECT DISTINCT id FROM m_actor_usr_legacy WHERE x_migrate AND usrname = xxx_barcode
1886         LOOP
1887             UPDATE m_actor_usr_legacy SET usrname = migration_schema || '_incumbent_usrname_collision_' || id || '_' || usrname WHERE id = xxx_id;
1888             GET DIAGNOSTICS row_count = ROW_COUNT;
1889             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
1890         END LOOP;
1891     END LOOP;
1892     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
1893 END
1894 $function$ LANGUAGE plpgsql;
1895
1896 -- alternate version: convenience function for handling item barcode collisions in m_asset_copy_legacy
1897
1898 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
1899 DECLARE
1900     xxx_barcode TEXT;
1901     xxx_id BIGINT;
1902     row_count NUMERIC;
1903     internal_collision_count NUMERIC := 0;
1904     incumbent_collision_count NUMERIC := 0;
1905 BEGIN
1906     FOR xxx_barcode IN SELECT barcode FROM m_asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1907     LOOP
1908         FOR xxx_id IN SELECT id FROM m_asset_copy WHERE barcode = xxx_barcode
1909         LOOP
1910             UPDATE m_asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = xxx_id;
1911             GET DIAGNOSTICS row_count = ROW_COUNT;
1912             internal_collision_count := internal_collision_count + row_count;
1913         END LOOP;
1914     END LOOP;
1915     RAISE INFO '% internal collisions', internal_collision_count;
1916     FOR xxx_barcode IN SELECT a.barcode FROM asset.copy a, m_asset_copy_legacy b WHERE x_migrate AND a.deleted IS FALSE AND a.barcode = b.barcode
1917     LOOP
1918         FOR xxx_id IN SELECT id FROM m_asset_copy_legacy WHERE barcode = xxx_barcode
1919         LOOP
1920             UPDATE m_asset_copy_legacy SET barcode = migration_schema || '_' || barcode WHERE id = xxx_id;
1921             GET DIAGNOSTICS row_count = ROW_COUNT;
1922             incumbent_collision_count := incumbent_collision_count + row_count;
1923         END LOOP;
1924     END LOOP;
1925     RAISE INFO '% incumbent collisions', incumbent_collision_count;
1926 END
1927 $function$ LANGUAGE plpgsql;
1928
1929 -- alternate version: convenience function for handling patron barcode/usrname collisions in m_actor_usr_legacy
1930 -- this should be ran prior to populating m_actor_card
1931
1932 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
1933 DECLARE
1934     xxx_barcode TEXT;
1935     xxx_id BIGINT;
1936     row_count NUMERIC;
1937     internal_collision_count NUMERIC := 0;
1938     incumbent_barcode_collision_count NUMERIC := 0;
1939     incumbent_usrname_collision_count NUMERIC := 0;
1940 BEGIN
1941     FOR xxx_barcode IN SELECT usrname FROM m_actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
1942     LOOP
1943         FOR xxx_id IN SELECT id FROM m_actor_usr_legacy WHERE x_migrate AND usrname = xxx_barcode
1944         LOOP
1945             UPDATE m_actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = xxx_id;
1946             GET DIAGNOSTICS row_count = ROW_COUNT;
1947             internal_collision_count := internal_collision_count + row_count;
1948         END LOOP;
1949     END LOOP;
1950     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
1951
1952     FOR xxx_barcode IN
1953         SELECT a.barcode FROM actor.card a, m_actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
1954     LOOP
1955         FOR xxx_id IN SELECT DISTINCT id FROM m_actor_usr_legacy WHERE x_migrate AND usrname = xxx_barcode
1956         LOOP
1957             UPDATE m_actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = xxx_id;
1958             GET DIAGNOSTICS row_count = ROW_COUNT;
1959             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
1960         END LOOP;
1961     END LOOP;
1962     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
1963
1964     FOR xxx_barcode IN
1965         SELECT a.usrname FROM actor.usr a, m_actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
1966     LOOP
1967         FOR xxx_id IN SELECT DISTINCT id FROM m_actor_usr_legacy WHERE x_migrate AND usrname = xxx_barcode
1968         LOOP
1969             UPDATE m_actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = xxx_id;
1970             GET DIAGNOSTICS row_count = ROW_COUNT;
1971             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
1972         END LOOP;
1973     END LOOP;
1974     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
1975 END
1976 $function$ LANGUAGE plpgsql;
1977
1978 CREATE OR REPLACE FUNCTION migration_tools.is_circ_rule_safe_to_delete( test_matchpoint INTEGER ) RETURNS BOOLEAN AS $func$
1979 -- WARNING: Use at your own risk
1980 -- FIXME: not considering marc_type, marc_form, marc_bib_level, marc_vr_format, usr_age_lower_bound, usr_age_upper_bound, item_age
1981 DECLARE
1982     item_object asset.copy%ROWTYPE;
1983     user_object actor.usr%ROWTYPE;
1984     test_rule_object config.circ_matrix_matchpoint%ROWTYPE;
1985     result_rule_object config.circ_matrix_matchpoint%ROWTYPE;
1986     safe_to_delete BOOLEAN := FALSE;
1987     m action.found_circ_matrix_matchpoint;
1988     n action.found_circ_matrix_matchpoint;
1989     -- ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] )
1990     result_matchpoint INTEGER;
1991 BEGIN
1992     SELECT INTO test_rule_object * FROM config.circ_matrix_matchpoint WHERE id = test_matchpoint;
1993     RAISE INFO 'testing rule: %', test_rule_object;
1994
1995     INSERT INTO actor.usr (
1996         profile,
1997         usrname,
1998         passwd,
1999         ident_type,
2000         first_given_name,
2001         family_name,
2002         home_ou,
2003         juvenile
2004     ) SELECT
2005         COALESCE(test_rule_object.grp, 2),
2006         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
2007         MD5(NOW()::TEXT),
2008         1,
2009         'Ima',
2010         'Test',
2011         COALESCE(test_rule_object.user_home_ou, test_rule_object.org_unit),
2012         COALESCE(test_rule_object.juvenile_flag, FALSE)
2013     ;
2014     
2015     SELECT INTO user_object * FROM actor.usr WHERE id = currval('actor.usr_id_seq');
2016
2017     INSERT INTO asset.call_number (
2018         creator,
2019         editor,
2020         record,
2021         owning_lib,
2022         label,
2023         label_class
2024     ) SELECT
2025         1,
2026         1,
2027         -1,
2028         COALESCE(test_rule_object.copy_owning_lib,test_rule_object.org_unit),
2029         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
2030         1
2031     ;
2032
2033     INSERT INTO asset.copy (
2034         barcode,
2035         circ_lib,
2036         creator,
2037         call_number,
2038         editor,
2039         location,
2040         loan_duration,
2041         fine_level,
2042         ref,
2043         circ_modifier
2044     ) SELECT
2045         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
2046         COALESCE(test_rule_object.copy_circ_lib,test_rule_object.org_unit),
2047         1,
2048         currval('asset.call_number_id_seq'),
2049         1,
2050         COALESCE(test_rule_object.copy_location,1),
2051         2,
2052         2,
2053         COALESCE(test_rule_object.ref_flag,FALSE),
2054         test_rule_object.circ_modifier
2055     ;
2056
2057     SELECT INTO item_object * FROM asset.copy WHERE id = currval('asset.copy_id_seq');
2058
2059     SELECT INTO m * FROM action.find_circ_matrix_matchpoint(
2060         test_rule_object.org_unit,
2061         item_object,
2062         user_object,
2063         COALESCE(test_rule_object.is_renewal,FALSE)
2064     );
2065     RAISE INFO '   action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
2066         test_rule_object.org_unit,
2067         item_object.id,
2068         user_object.id,
2069         COALESCE(test_rule_object.is_renewal,FALSE),
2070         m.success,
2071         m.matchpoint,
2072         m.buildrows
2073     ;
2074
2075     --  disable the rule being tested to see if the outcome changes
2076     UPDATE config.circ_matrix_matchpoint SET active = FALSE WHERE id = (m.matchpoint).id;
2077
2078     SELECT INTO n * FROM action.find_circ_matrix_matchpoint(
2079         test_rule_object.org_unit,
2080         item_object,
2081         user_object,
2082         COALESCE(test_rule_object.is_renewal,FALSE)
2083     );
2084     RAISE INFO 'VS action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
2085         test_rule_object.org_unit,
2086         item_object.id,
2087         user_object.id,
2088         COALESCE(test_rule_object.is_renewal,FALSE),
2089         n.success,
2090         n.matchpoint,
2091         n.buildrows
2092     ;
2093
2094     -- FIXME: We could dig deeper and see if the referenced config.rule_*
2095     -- entries are effectively equivalent, but for now, let's assume no
2096     -- duplicate rules at that level
2097     IF (
2098             (m.matchpoint).circulate = (n.matchpoint).circulate
2099         AND (m.matchpoint).duration_rule = (n.matchpoint).duration_rule
2100         AND (m.matchpoint).recurring_fine_rule = (n.matchpoint).recurring_fine_rule
2101         AND (m.matchpoint).max_fine_rule = (n.matchpoint).max_fine_rule
2102         AND (
2103                 (m.matchpoint).hard_due_date = (n.matchpoint).hard_due_date
2104                 OR (
2105                         (m.matchpoint).hard_due_date IS NULL
2106                     AND (n.matchpoint).hard_due_date IS NULL
2107                 )
2108         )
2109         AND (
2110                 (m.matchpoint).renewals = (n.matchpoint).renewals
2111                 OR (
2112                         (m.matchpoint).renewals IS NULL
2113                     AND (n.matchpoint).renewals IS NULL
2114                 )
2115         )
2116         AND (
2117                 (m.matchpoint).grace_period = (n.matchpoint).grace_period
2118                 OR (
2119                         (m.matchpoint).grace_period IS NULL
2120                     AND (n.matchpoint).grace_period IS NULL
2121                 )
2122         )
2123         AND (
2124                 (m.matchpoint).total_copy_hold_ratio = (n.matchpoint).total_copy_hold_ratio
2125                 OR (
2126                         (m.matchpoint).total_copy_hold_ratio IS NULL
2127                     AND (n.matchpoint).total_copy_hold_ratio IS NULL
2128                 )
2129         )
2130         AND (
2131                 (m.matchpoint).available_copy_hold_ratio = (n.matchpoint).available_copy_hold_ratio
2132                 OR (
2133                         (m.matchpoint).available_copy_hold_ratio IS NULL
2134                     AND (n.matchpoint).available_copy_hold_ratio IS NULL
2135                 )
2136         )
2137         AND NOT EXISTS (
2138             SELECT limit_set, fallthrough
2139             FROM config.circ_matrix_limit_set_map
2140             WHERE active and matchpoint = (m.matchpoint).id
2141             EXCEPT
2142             SELECT limit_set, fallthrough
2143             FROM config.circ_matrix_limit_set_map
2144             WHERE active and matchpoint = (n.matchpoint).id
2145         )
2146
2147     ) THEN
2148         RAISE INFO 'rule has same outcome';
2149         safe_to_delete := TRUE;
2150     ELSE
2151         RAISE INFO 'rule has different outcome';
2152         safe_to_delete := FALSE;
2153     END IF;
2154
2155     RAISE EXCEPTION 'rollback the temporary changes';
2156
2157 EXCEPTION WHEN OTHERS THEN
2158
2159     RAISE INFO 'inside exception block: %, %', SQLSTATE, SQLERRM;
2160     RETURN safe_to_delete;
2161
2162 END;
2163 $func$ LANGUAGE plpgsql;
2164