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