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