ed9bba42afe6b938eed6f2c99dfc03d76c57ecca
[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 CREATE OR REPLACE FUNCTION migration_tools.create_linked_legacy_table_from (TEXT,TEXT,TEXT) RETURNS VOID AS $$
265     DECLARE
266         migration_schema ALIAS FOR $1;
267         parent_table ALIAS FOR $2;
268         source_table ALIAS FOR $3;
269         columns RECORD;
270         create_sql TEXT;
271         insert_sql TEXT;
272         column_list TEXT := '';
273         column_count INTEGER := 0;
274     BEGIN
275         create_sql := 'CREATE TABLE ' || migration_schema || '.' || parent_table || '_legacy ( ';
276         FOR columns IN
277             SELECT table_schema, table_name, column_name, data_type
278             FROM information_schema.columns
279             WHERE table_schema = migration_schema AND table_name = source_table
280         LOOP
281             column_count := column_count + 1;
282             if column_count > 1 then
283                 create_sql := create_sql || ', ';
284                 column_list := column_list || ', ';
285             end if;
286             create_sql := create_sql || columns.column_name || ' ';
287             if columns.data_type = 'ARRAY' then
288                 create_sql := create_sql || 'TEXT[]';
289             else
290                 create_sql := create_sql || columns.data_type;
291             end if;
292             column_list := column_list || columns.column_name;
293         END LOOP;
294         create_sql := create_sql || ' ) INHERITS ( ' || migration_schema || '.' || parent_table || ' );';
295         --RAISE INFO 'create_sql = %', create_sql;
296         EXECUTE create_sql;
297         insert_sql := 'INSERT INTO ' || migration_schema || '.' || parent_table || '_legacy (' || column_list || ') SELECT ' || column_list || ' FROM ' || migration_schema || '.' || source_table || ';';
298         --RAISE INFO 'insert_sql = %', insert_sql;
299         EXECUTE insert_sql;
300     END;
301 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
302
303 CREATE OR REPLACE FUNCTION migration_tools.insert_base_into_production (TEXT) RETURNS VOID AS $$
304     DECLARE
305         migration_schema ALIAS FOR $1;
306         production_tables TEXT[];
307     BEGIN
308         --RAISE INFO 'In migration_tools.insert_into_production(%)', migration_schema;
309         SELECT migration_tools.production_tables(migration_schema) INTO STRICT production_tables;
310         FOR i IN array_lower(production_tables,1) .. array_upper(production_tables,1) LOOP
311             PERFORM migration_tools.insert_into_production(migration_schema,production_tables[i]);
312         END LOOP;
313     END;
314 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
315
316 CREATE OR REPLACE FUNCTION migration_tools.insert_into_production (TEXT,TEXT) RETURNS VOID AS $$
317     DECLARE
318         migration_schema ALIAS FOR $1;
319         production_table ALIAS FOR $2;
320         base_staging_table TEXT;
321         columns RECORD;
322     BEGIN
323         base_staging_table = REPLACE( production_table, '.', '_' );
324         --RAISE INFO 'In migration_tools.insert_into_production(%,%) -> %', migration_schema, production_table, base_staging_table;
325         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || production_table || ' SELECT * FROM ' || migration_schema || '.' || base_staging_table || ';' );
326     END;
327 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
328
329 CREATE OR REPLACE FUNCTION migration_tools.name_parse_out_first_middle_last_comma_suffix (TEXT) RETURNS TEXT[] AS $$
330     DECLARE
331         full_name TEXT := $1;
332         before_comma TEXT;
333         family_name TEXT := '';
334         first_given_name TEXT := '';
335         second_given_name TEXT := '';
336         suffix TEXT := '';
337         prefix TEXT := '';
338     BEGIN
339         before_comma := BTRIM( REGEXP_REPLACE(full_name,E'^(.+),.+$',E'\\1') );
340         suffix := CASE WHEN full_name ~ ',' THEN BTRIM( REGEXP_REPLACE(full_name,E'^.+,(.+)$',E'\\1') ) ELSE '' END;
341
342         IF suffix = before_comma THEN
343             suffix := '';
344         END IF;
345
346         family_name := BTRIM( REGEXP_REPLACE(before_comma,E'^.+\\s(.+)$',E'\\1') );
347         first_given_name := BTRIM( REGEXP_REPLACE(before_comma,E'^(.+?)\\s.+$',E'\\1') );
348         second_given_name := BTRIM( CASE WHEN before_comma ~ '^.+\s.+\s.+$' THEN REGEXP_REPLACE(before_comma,E'^.+\\s(.+)\\s.+$',E'\\1') ELSE '' END );
349
350         RETURN ARRAY[ family_name, prefix, first_given_name, second_given_name, suffix ];
351     END;
352 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
353
354 CREATE OR REPLACE FUNCTION migration_tools.name_parse_out_last_comma_prefix_first_middle_suffix (TEXT) RETURNS TEXT[] AS $$
355     DECLARE
356         full_name TEXT := $1;
357         temp TEXT;
358         family_name TEXT := '';
359         first_given_name TEXT := '';
360         second_given_name TEXT := '';
361         suffix TEXT := '';
362         prefix TEXT := '';
363     BEGIN
364         temp := full_name;
365         -- Use values, not structure, for prefix/suffix, unless we come up with a better idea
366         IF temp ilike '%MR.%' THEN
367             prefix := 'Mr.';
368             temp := REGEXP_REPLACE( temp, E'MR\.\\s*', '', 'i' );
369         END IF;
370         IF temp ilike '%MRS.%' THEN
371             prefix := 'Mrs.';
372             temp := REGEXP_REPLACE( temp, E'MRS\.\\s*', '', 'i' );
373         END IF;
374         IF temp ilike '%MS.%' THEN
375             prefix := 'Ms.';
376             temp := REGEXP_REPLACE( temp, E'MS\.\\s*', '', 'i' );
377         END IF;
378         IF temp ilike '%DR.%' THEN
379             prefix := 'Dr.';
380             temp := REGEXP_REPLACE( temp, E'DR\.\\s*', '', 'i' );
381         END IF;
382         IF temp ilike '%JR%' THEN
383             suffix := 'Jr.';
384             temp := REGEXP_REPLACE( temp, E'JR\.?\\s*', '', 'i' );
385         END IF;
386         IF temp ilike '%JR,%' THEN
387             suffix := 'Jr.';
388             temp := REGEXP_REPLACE( temp, E'JR,\\s*', ',', 'i' );
389         END IF;
390         IF temp ilike '%SR%' THEN
391             suffix := 'Sr.';
392             temp := REGEXP_REPLACE( temp, E'SR\.?\\s*', '', 'i' );
393         END IF;
394         IF temp ilike '%SR,%' THEN
395             suffix := 'Sr.';
396             temp := REGEXP_REPLACE( temp, E'SR,\\s*', ',', 'i' );
397         END IF;
398         IF temp ~ E'\\sII$' THEN
399             suffix := 'II';
400             temp := REGEXP_REPLACE( temp, E'II$', '', 'i' );
401         END IF;
402         IF temp ~ E'\\sIII$' THEN
403             suffix := 'III';
404             temp := REGEXP_REPLACE( temp, E'III$', '', 'i' );
405         END IF;
406         IF temp ~ E'\\sIV$' THEN
407             suffix := 'IV';
408             temp := REGEXP_REPLACE( temp, E'IV$', '', 'i' );
409         END IF;
410
411         family_name := BTRIM( REGEXP_REPLACE(temp,E'^([^,]*)\\s*,.*$',E'\\1') );
412         first_given_name := BTRIM( CASE WHEN temp ~ ',' THEN REGEXP_REPLACE(temp,E'^[^,]*\\s*,\\s*([^,\\s]*)\\s*.*$',E'\\1') ELSE 'N/A' END );
413         second_given_name := BTRIM( CASE WHEN temp ~ ',' THEN REGEXP_REPLACE(temp,E'^[^,]*\\s*,\\s*[^,\\s]*\\s*(.*)$',E'\\1') ELSE ''  END );
414
415         RETURN ARRAY[ family_name, prefix, first_given_name, second_given_name, suffix ];
416     END;
417 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
418
419 CREATE OR REPLACE FUNCTION migration_tools.name_parse_out_last_first_middle_and_random_affix (TEXT) RETURNS TEXT[] AS $$
420     DECLARE
421         full_name TEXT := $1;
422         temp TEXT;
423         family_name TEXT := '';
424         first_given_name TEXT := '';
425         second_given_name TEXT := '';
426         suffix TEXT := '';
427         prefix TEXT := '';
428     BEGIN
429         temp := full_name;
430         -- Use values, not structure, for prefix/suffix, unless we come up with a better idea
431         IF temp ilike '%MR.%' THEN
432             prefix := 'Mr.';
433             temp := REGEXP_REPLACE( temp, E'MR\.\\s*', '', 'i' );
434         END IF;
435         IF temp ilike '%MRS.%' THEN
436             prefix := 'Mrs.';
437             temp := REGEXP_REPLACE( temp, E'MRS\.\\s*', '', 'i' );
438         END IF;
439         IF temp ilike '%MS.%' THEN
440             prefix := 'Ms.';
441             temp := REGEXP_REPLACE( temp, E'MS\.\\s*', '', 'i' );
442         END IF;
443         IF temp ilike '%DR.%' THEN
444             prefix := 'Dr.';
445             temp := REGEXP_REPLACE( temp, E'DR\.\\s*', '', 'i' );
446         END IF;
447         IF temp ilike '%JR.%' THEN
448             suffix := 'Jr.';
449             temp := REGEXP_REPLACE( temp, E'JR\.\\s*', '', 'i' );
450         END IF;
451         IF temp ilike '%JR,%' THEN
452             suffix := 'Jr.';
453             temp := REGEXP_REPLACE( temp, E'JR,\\s*', ',', 'i' );
454         END IF;
455         IF temp ilike '%SR.%' THEN
456             suffix := 'Sr.';
457             temp := REGEXP_REPLACE( temp, E'SR\.\\s*', '', 'i' );
458         END IF;
459         IF temp ilike '%SR,%' THEN
460             suffix := 'Sr.';
461             temp := REGEXP_REPLACE( temp, E'SR,\\s*', ',', 'i' );
462         END IF;
463         IF temp like '%III%' THEN
464             suffix := 'III';
465             temp := REGEXP_REPLACE( temp, E'III', '' );
466         END IF;
467         IF temp like '%II%' THEN
468             suffix := 'II';
469             temp := REGEXP_REPLACE( temp, E'II', '' );
470         END IF;
471         IF temp like '%IV%' THEN
472             suffix := 'IV';
473             temp := REGEXP_REPLACE( temp, E'IV', '' );
474         END IF;
475
476         temp := REGEXP_REPLACE( temp, '\(\)', '');
477         family_name := BTRIM( REGEXP_REPLACE(temp,E'^(\\S+).*$',E'\\1') );
478         family_name := REGEXP_REPLACE( family_name, ',', '' );
479         first_given_name := CASE WHEN temp ~ E'^\\S+$' THEN 'N/A' ELSE BTRIM( REGEXP_REPLACE(temp,E'^\\S+\\s+(\\S+).*$',E'\\1') ) END;
480         first_given_name := REGEXP_REPLACE( first_given_name, ',', '' );
481         second_given_name := CASE WHEN temp ~ E'^\\S+$' THEN '' ELSE BTRIM( REGEXP_REPLACE(temp,E'^\\S+\\s+\\S+\\s*(.*)$',E'\\1') ) END;
482         second_given_name := REGEXP_REPLACE( second_given_name, ',', '' );
483
484         RETURN ARRAY[ family_name, prefix, first_given_name, second_given_name, suffix ];
485     END;
486 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
487
488 CREATE OR REPLACE FUNCTION migration_tools.name_parse_out_fuller_last_first_middle_and_random_affix (TEXT) RETURNS TEXT[] AS $$
489     DECLARE
490         full_name TEXT := $1;
491         temp TEXT;
492         family_name TEXT := '';
493         first_given_name TEXT := '';
494         second_given_name TEXT := '';
495         suffix TEXT := '';
496         prefix TEXT := '';
497     BEGIN
498         temp := BTRIM(full_name);
499         -- Use values, not structure, for prefix/suffix, unless we come up with a better idea
500         --IF temp ~ '^\S{2,}\.' THEN
501         --    prefix := REGEXP_REPLACE(temp, '^(\S{2,}\.).*$','\1');
502         --    temp := BTRIM(REGEXP_REPLACE(temp, '^\S{2,}\.(.*)$','\1'));
503         --END IF;
504         --IF temp ~ '\S{2,}\.$' THEN
505         --    suffix := REGEXP_REPLACE(temp, '^.*(\S{2,}\.)$','\1');
506         --    temp := REGEXP_REPLACE(temp, '^(.*)\S{2,}\.$','\1');
507         --END IF;
508         IF temp ilike '%MR.%' THEN
509             prefix := 'Mr.';
510             temp := BTRIM(REGEXP_REPLACE( temp, E'MR\.\\s*', '', 'i' ));
511         END IF;
512         IF temp ilike '%MRS.%' THEN
513             prefix := 'Mrs.';
514             temp := BTRIM(REGEXP_REPLACE( temp, E'MRS\.\\s*', '', 'i' ));
515         END IF;
516         IF temp ilike '%MS.%' THEN
517             prefix := 'Ms.';
518             temp := BTRIM(REGEXP_REPLACE( temp, E'MS\.\\s*', '', 'i' ));
519         END IF;
520         IF temp ilike '%DR.%' THEN
521             prefix := 'Dr.';
522             temp := BTRIM(REGEXP_REPLACE( temp, E'DR\.\\s*', '', 'i' ));
523         END IF;
524         IF temp ilike '%JR.%' THEN
525             suffix := 'Jr.';
526             temp := BTRIM(REGEXP_REPLACE( temp, E'JR\.\\s*', '', 'i' ));
527         END IF;
528         IF temp ilike '%JR,%' THEN
529             suffix := 'Jr.';
530             temp := BTRIM(REGEXP_REPLACE( temp, E'JR,\\s*', ',', 'i' ));
531         END IF;
532         IF temp ilike '%SR.%' THEN
533             suffix := 'Sr.';
534             temp := BTRIM(REGEXP_REPLACE( temp, E'SR\.\\s*', '', 'i' ));
535         END IF;
536         IF temp ilike '%SR,%' THEN
537             suffix := 'Sr.';
538             temp := BTRIM(REGEXP_REPLACE( temp, E'SR,\\s*', ',', 'i' ));
539         END IF;
540         IF temp like '%III%' THEN
541             suffix := 'III';
542             temp := BTRIM(REGEXP_REPLACE( temp, E'III', '' ));
543         END IF;
544         IF temp like '%II%' THEN
545             suffix := 'II';
546             temp := BTRIM(REGEXP_REPLACE( temp, E'II', '' ));
547         END IF;
548
549         IF temp ~ ',' THEN
550             family_name = BTRIM(REGEXP_REPLACE(temp,'^(.*?,).*$','\1'));
551             temp := BTRIM(REPLACE( temp, family_name, '' ));
552             family_name := REPLACE( family_name, ',', '' );
553             IF temp ~ ' ' THEN
554                 first_given_name := BTRIM( REGEXP_REPLACE(temp,'^(.+)\s(.+)$','\1') );
555                 second_given_name := BTRIM( REGEXP_REPLACE(temp,'^(.+)\s(.+)$','\2') );
556             ELSE
557                 first_given_name := temp;
558                 second_given_name := '';
559             END IF;
560         ELSE
561             IF temp ~ '^\S+\s+\S+\s+\S+$' THEN
562                 first_given_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)\s*(\S+)$','\1') );
563                 second_given_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)\s*(\S+)$','\2') );
564                 family_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)\s*(\S+)$','\3') );
565             ELSE
566                 first_given_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)$','\1') );
567                 second_given_name := temp;
568                 family_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)$','\2') );
569             END IF;
570         END IF;
571
572         RETURN ARRAY[ family_name, prefix, first_given_name, second_given_name, suffix ];
573     END;
574 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
575
576 CREATE OR REPLACE FUNCTION migration_tools.name_parse_out_fuller_last_first_middle_and_random_affix2 (TEXT) RETURNS TEXT[] AS $$
577     DECLARE
578         full_name TEXT := $1;
579         temp TEXT;
580         family_name TEXT := '';
581         first_given_name TEXT := '';
582         second_given_name TEXT := '';
583         suffix TEXT := '';
584         prefix TEXT := '';
585     BEGIN
586         temp := BTRIM(full_name);
587         -- Use values, not structure, for prefix/suffix, unless we come up with a better idea
588         --IF temp ~ '^\S{2,}\.' THEN
589         --    prefix := REGEXP_REPLACE(temp, '^(\S{2,}\.).*$','\1');
590         --    temp := BTRIM(REGEXP_REPLACE(temp, '^\S{2,}\.(.*)$','\1'));
591         --END IF;
592         --IF temp ~ '\S{2,}\.$' THEN
593         --    suffix := REGEXP_REPLACE(temp, '^.*(\S{2,}\.)$','\1');
594         --    temp := REGEXP_REPLACE(temp, '^(.*)\S{2,}\.$','\1');
595         --END IF;
596         IF temp ilike '%MR.%' THEN
597             prefix := 'Mr.';
598             temp := BTRIM(REGEXP_REPLACE( temp, E'MR\.\\s*', '', 'i' ));
599         END IF;
600         IF temp ilike '%MRS.%' THEN
601             prefix := 'Mrs.';
602             temp := BTRIM(REGEXP_REPLACE( temp, E'MRS\.\\s*', '', 'i' ));
603         END IF;
604         IF temp ilike '%MS.%' THEN
605             prefix := 'Ms.';
606             temp := BTRIM(REGEXP_REPLACE( temp, E'MS\.\\s*', '', 'i' ));
607         END IF;
608         IF temp ilike '%DR.%' THEN
609             prefix := 'Dr.';
610             temp := BTRIM(REGEXP_REPLACE( temp, E'DR\.\\s*', '', 'i' ));
611         END IF;
612         IF temp ilike '%JR.%' THEN
613             suffix := 'Jr.';
614             temp := BTRIM(REGEXP_REPLACE( temp, E'JR\.\\s*', '', 'i' ));
615         END IF;
616         IF temp ilike '%JR,%' THEN
617             suffix := 'Jr.';
618             temp := BTRIM(REGEXP_REPLACE( temp, E'JR,\\s*', ',', 'i' ));
619         END IF;
620         IF temp ilike '%SR.%' THEN
621             suffix := 'Sr.';
622             temp := BTRIM(REGEXP_REPLACE( temp, E'SR\.\\s*', '', 'i' ));
623         END IF;
624         IF temp ilike '%SR,%' THEN
625             suffix := 'Sr.';
626             temp := BTRIM(REGEXP_REPLACE( temp, E'SR,\\s*', ',', 'i' ));
627         END IF;
628         IF temp like '%III%' THEN
629             suffix := 'III';
630             temp := BTRIM(REGEXP_REPLACE( temp, E'III', '' ));
631         END IF;
632         IF temp like '%II%' THEN
633             suffix := 'II';
634             temp := BTRIM(REGEXP_REPLACE( temp, E'II', '' ));
635         END IF;
636
637         IF temp ~ ',' THEN
638             family_name = BTRIM(REGEXP_REPLACE(temp,'^(.*?,).*$','\1'));
639             temp := BTRIM(REPLACE( temp, family_name, '' ));
640             family_name := REPLACE( family_name, ',', '' );
641             IF temp ~ ' ' THEN
642                 first_given_name := BTRIM( REGEXP_REPLACE(temp,'^(.+)\s(.+)$','\1') );
643                 second_given_name := BTRIM( REGEXP_REPLACE(temp,'^(.+)\s(.+)$','\2') );
644             ELSE
645                 first_given_name := temp;
646                 second_given_name := '';
647             END IF;
648         ELSE
649             IF temp ~ '^\S+\s+\S+\s+\S+$' THEN
650                 first_given_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)\s*(\S+)$','\1') );
651                 second_given_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)\s*(\S+)$','\2') );
652                 family_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)\s*(\S+)$','\3') );
653             ELSE
654                 first_given_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)$','\1') );
655                 second_given_name := temp;
656                 family_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)$','\2') );
657             END IF;
658         END IF;
659
660         family_name := BTRIM(REPLACE(REPLACE(family_name,',',''),'"',''));
661         first_given_name := BTRIM(REPLACE(REPLACE(first_given_name,',',''),'"',''));
662         second_given_name := BTRIM(REPLACE(REPLACE(second_given_name,',',''),'"',''));
663
664         RETURN ARRAY[ family_name, prefix, first_given_name, second_given_name, suffix ];
665     END;
666 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
667
668 CREATE OR REPLACE FUNCTION migration_tools.address_parse_out_citystatezip (TEXT) RETURNS TEXT[] AS $$
669     DECLARE
670         city_state_zip TEXT := $1;
671         city TEXT := '';
672         state TEXT := '';
673         zip TEXT := '';
674     BEGIN
675         zip := CASE WHEN city_state_zip ~ E'\\d\\d\\d\\d\\d' THEN REGEXP_REPLACE( city_state_zip, E'^.*(\\d\\d\\d\\d\\d-?\\d*).*$', E'\\1' ) ELSE '' END;
676         city_state_zip := REGEXP_REPLACE( city_state_zip, E'^(.*)\\d\\d\\d\\d\\d-?\\d*(.*)$', E'\\1\\2');
677         IF city_state_zip ~ ',' THEN
678             state := REGEXP_REPLACE( city_state_zip, E'^(.*),(.*)$', E'\\2');
679             city := REGEXP_REPLACE( city_state_zip, E'^(.*),(.*)$', E'\\1');
680         ELSE
681             IF city_state_zip ~ E'\\s+[A-Z][A-Z]\\s*$' THEN
682                 state := REGEXP_REPLACE( city_state_zip, E'^.*,?\\s+([A-Z][A-Z])\\s*$', E'\\1' );
683                 city := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s+[A-Z][A-Z](\\s*)$', E'\\1\\2' );
684             ELSE
685                 IF city_state_zip ~ E'^\\S+$'  THEN
686                     city := city_state_zip;
687                     state := 'N/A';
688                 ELSE
689                     state := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\2');
690                     city := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\1');
691                 END IF;
692             END IF;
693         END IF;
694         RETURN ARRAY[ TRIM(BOTH ' ' FROM city), TRIM(BOTH ' ' FROM state), TRIM(BOTH ' ' FROM zip) ];
695     END;
696 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
697
698 -- try to parse data like this: 100 MAIN ST$COVINGTON, GA 30016
699 CREATE OR REPLACE FUNCTION migration_tools.parse_out_address (TEXT) RETURNS TEXT[] AS $$
700     DECLARE
701         fullstring TEXT := $1;
702         address1 TEXT := '';
703         address2 TEXT := '';
704         scratch1 TEXT := '';
705         scratch2 TEXT := '';
706         city TEXT := '';
707         state TEXT := '';
708         zip TEXT := '';
709     BEGIN
710         zip := CASE
711             WHEN fullstring ~ E'\\d\\d\\d\\d\\d'
712             THEN REGEXP_REPLACE( fullstring, E'^.*(\\d\\d\\d\\d\\d-?\\d*).*$', E'\\1' )
713             ELSE ''
714         END;
715         fullstring := REGEXP_REPLACE( fullstring, E'^(.*)\\d\\d\\d\\d\\d-?\\d*(.*)$', E'\\1\\2');
716
717         IF fullstring ~ ',' THEN
718             state := REGEXP_REPLACE( fullstring, E'^(.*),(.*)$', E'\\2');
719             scratch1 := REGEXP_REPLACE( fullstring, E'^(.*),(.*)$', E'\\1');
720         ELSE
721             IF fullstring ~ E'\\s+[A-Z][A-Z]\\s*$' THEN
722                 state := REGEXP_REPLACE( fullstring, E'^.*,?\\s+([A-Z][A-Z])\\s*$', E'\\1' );
723                 scratch1 := REGEXP_REPLACE( fullstring, E'^(.*?),?\\s+[A-Z][A-Z](\\s*)$', E'\\1\\2' );
724             ELSE
725                 IF fullstring ~ E'^\\S+$'  THEN
726                     scratch1 := fullstring;
727                     state := 'N/A';
728                 ELSE
729                     state := REGEXP_REPLACE( fullstring, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\2');
730                     scratch1 := REGEXP_REPLACE( fullstring, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\1');
731                 END IF;
732             END IF;
733         END IF;
734
735         IF scratch1 ~ '[\$]' THEN
736             scratch2 := REGEXP_REPLACE( scratch1, E'^(.+)[\$](.+?)$', E'\\1');
737             city := REGEXP_REPLACE( scratch1, E'^(.+)[\$](.+?)$', E'\\2');
738         ELSE
739             IF scratch1 ~ '\s' THEN
740                 scratch2 := REGEXP_REPLACE( scratch1, E'^(.+)\\s+(.+?)$', E'\\1');
741                 city := REGEXP_REPLACE( scratch1, E'^(.+)\\s+(.+?)$', E'\\2');
742             ELSE
743                 scratch2 := 'N/A';
744                 city := scratch1;
745             END IF;
746         END IF;
747
748         IF scratch2 ~ '^\d' THEN
749             address1 := scratch2;
750             address2 := '';
751         ELSE
752             address1 := REGEXP_REPLACE( scratch2, E'^(.+?)(\\d.+)$', E'\\1');
753             address2 := REGEXP_REPLACE( scratch2, E'^(.+?)(\\d.+)$', E'\\2');
754         END IF;
755
756         RETURN ARRAY[
757              TRIM(BOTH ' ' FROM address1)
758             ,TRIM(BOTH ' ' FROM address2)
759             ,TRIM(BOTH ' ' FROM city)
760             ,TRIM(BOTH ' ' FROM state)
761             ,TRIM(BOTH ' ' FROM zip)
762         ];
763     END;
764 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
765
766 CREATE OR REPLACE FUNCTION migration_tools.parse_out_address2 (TEXT) RETURNS TEXT AS $$
767     my ($address) = @_;
768
769     use Geo::StreetAddress::US;
770
771     my $a = Geo::StreetAddress::US->parse_location($address);
772
773     return [
774          "$a->{number} $a->{prefix} $a->{street} $a->{type} $a->{suffix}"
775         ,"$a->{sec_unit_type} $a->{sec_unit_num}"
776         ,$a->{city}
777         ,$a->{state}
778         ,$a->{zip}
779     ];
780 $$ LANGUAGE PLPERLU STABLE;
781
782 DROP TABLE IF EXISTS migration_tools.usps_suffixes;
783 CREATE TABLE migration_tools.usps_suffixes ( suffix_from TEXT, suffix_to TEXT );
784 INSERT INTO migration_tools.usps_suffixes VALUES
785     ('ALLEE','ALY'),
786     ('ALLEY','ALY'),
787     ('ALLY','ALY'),
788     ('ALY','ALY'),
789     ('ANEX','ANX'),
790     ('ANNEX','ANX'),
791     ('ANNX','ANX'),
792     ('ANX','ANX'),
793     ('ARCADE','ARC'),
794     ('ARC','ARC'),
795     ('AV','AVE'),
796     ('AVE','AVE'),
797     ('AVEN','AVE'),
798     ('AVENU','AVE'),
799     ('AVENUE','AVE'),
800     ('AVN','AVE'),
801     ('AVNUE','AVE'),
802     ('BAYOO','BYU'),
803     ('BAYOU','BYU'),
804     ('BCH','BCH'),
805     ('BEACH','BCH'),
806     ('BEND','BND'),
807     ('BLF','BLF'),
808     ('BLUF','BLF'),
809     ('BLUFF','BLF'),
810     ('BLUFFS','BLFS'),
811     ('BLVD','BLVD'),
812     ('BND','BND'),
813     ('BOT','BTM'),
814     ('BOTTM','BTM'),
815     ('BOTTOM','BTM'),
816     ('BOUL','BLVD'),
817     ('BOULEVARD','BLVD'),
818     ('BOULV','BLVD'),
819     ('BRANCH','BR'),
820     ('BR','BR'),
821     ('BRDGE','BRG'),
822     ('BRG','BRG'),
823     ('BRIDGE','BRG'),
824     ('BRK','BRK'),
825     ('BRNCH','BR'),
826     ('BROOK','BRK'),
827     ('BROOKS','BRKS'),
828     ('BTM','BTM'),
829     ('BURG','BG'),
830     ('BURGS','BGS'),
831     ('BYPA','BYP'),
832     ('BYPAS','BYP'),
833     ('BYPASS','BYP'),
834     ('BYP','BYP'),
835     ('BYPS','BYP'),
836     ('CAMP','CP'),
837     ('CANYN','CYN'),
838     ('CANYON','CYN'),
839     ('CAPE','CPE'),
840     ('CAUSEWAY','CSWY'),
841     ('CAUSWAY','CSWY'),
842     ('CEN','CTR'),
843     ('CENT','CTR'),
844     ('CENTER','CTR'),
845     ('CENTERS','CTRS'),
846     ('CENTR','CTR'),
847     ('CENTRE','CTR'),
848     ('CIRC','CIR'),
849     ('CIR','CIR'),
850     ('CIRCL','CIR'),
851     ('CIRCLE','CIR'),
852     ('CIRCLES','CIRS'),
853     ('CK','CRK'),
854     ('CLB','CLB'),
855     ('CLF','CLF'),
856     ('CLFS','CLFS'),
857     ('CLIFF','CLF'),
858     ('CLIFFS','CLFS'),
859     ('CLUB','CLB'),
860     ('CMP','CP'),
861     ('CNTER','CTR'),
862     ('CNTR','CTR'),
863     ('CNYN','CYN'),
864     ('COMMON','CMN'),
865     ('COR','COR'),
866     ('CORNER','COR'),
867     ('CORNERS','CORS'),
868     ('CORS','CORS'),
869     ('COURSE','CRSE'),
870     ('COURT','CT'),
871     ('COURTS','CTS'),
872     ('COVE','CV'),
873     ('COVES','CVS'),
874     ('CP','CP'),
875     ('CPE','CPE'),
876     ('CRCL','CIR'),
877     ('CRCLE','CIR'),
878     ('CR','CRK'),
879     ('CRECENT','CRES'),
880     ('CREEK','CRK'),
881     ('CRESCENT','CRES'),
882     ('CRES','CRES'),
883     ('CRESENT','CRES'),
884     ('CREST','CRST'),
885     ('CRK','CRK'),
886     ('CROSSING','XING'),
887     ('CROSSROAD','XRD'),
888     ('CRSCNT','CRES'),
889     ('CRSE','CRSE'),
890     ('CRSENT','CRES'),
891     ('CRSNT','CRES'),
892     ('CRSSING','XING'),
893     ('CRSSNG','XING'),
894     ('CRT','CT'),
895     ('CSWY','CSWY'),
896     ('CT','CT'),
897     ('CTR','CTR'),
898     ('CTS','CTS'),
899     ('CURVE','CURV'),
900     ('CV','CV'),
901     ('CYN','CYN'),
902     ('DALE','DL'),
903     ('DAM','DM'),
904     ('DIV','DV'),
905     ('DIVIDE','DV'),
906     ('DL','DL'),
907     ('DM','DM'),
908     ('DR','DR'),
909     ('DRIV','DR'),
910     ('DRIVE','DR'),
911     ('DRIVES','DRS'),
912     ('DRV','DR'),
913     ('DVD','DV'),
914     ('DV','DV'),
915     ('ESTATE','EST'),
916     ('ESTATES','ESTS'),
917     ('EST','EST'),
918     ('ESTS','ESTS'),
919     ('EXP','EXPY'),
920     ('EXPRESS','EXPY'),
921     ('EXPRESSWAY','EXPY'),
922     ('EXPR','EXPY'),
923     ('EXPW','EXPY'),
924     ('EXPY','EXPY'),
925     ('EXTENSION','EXT'),
926     ('EXTENSIONS','EXTS'),
927     ('EXT','EXT'),
928     ('EXTN','EXT'),
929     ('EXTNSN','EXT'),
930     ('EXTS','EXTS'),
931     ('FALL','FALL'),
932     ('FALLS','FLS'),
933     ('FERRY','FRY'),
934     ('FIELD','FLD'),
935     ('FIELDS','FLDS'),
936     ('FLAT','FLT'),
937     ('FLATS','FLTS'),
938     ('FLD','FLD'),
939     ('FLDS','FLDS'),
940     ('FLS','FLS'),
941     ('FLT','FLT'),
942     ('FLTS','FLTS'),
943     ('FORD','FRD'),
944     ('FORDS','FRDS'),
945     ('FOREST','FRST'),
946     ('FORESTS','FRST'),
947     ('FORGE','FRG'),
948     ('FORGES','FRGS'),
949     ('FORG','FRG'),
950     ('FORK','FRK'),
951     ('FORKS','FRKS'),
952     ('FORT','FT'),
953     ('FRD','FRD'),
954     ('FREEWAY','FWY'),
955     ('FREEWY','FWY'),
956     ('FRG','FRG'),
957     ('FRK','FRK'),
958     ('FRKS','FRKS'),
959     ('FRRY','FRY'),
960     ('FRST','FRST'),
961     ('FRT','FT'),
962     ('FRWAY','FWY'),
963     ('FRWY','FWY'),
964     ('FRY','FRY'),
965     ('FT','FT'),
966     ('FWY','FWY'),
967     ('GARDEN','GDN'),
968     ('GARDENS','GDNS'),
969     ('GARDN','GDN'),
970     ('GATEWAY','GTWY'),
971     ('GATEWY','GTWY'),
972     ('GATWAY','GTWY'),
973     ('GDN','GDN'),
974     ('GDNS','GDNS'),
975     ('GLEN','GLN'),
976     ('GLENS','GLNS'),
977     ('GLN','GLN'),
978     ('GRDEN','GDN'),
979     ('GRDN','GDN'),
980     ('GRDNS','GDNS'),
981     ('GREEN','GRN'),
982     ('GREENS','GRNS'),
983     ('GRN','GRN'),
984     ('GROVE','GRV'),
985     ('GROVES','GRVS'),
986     ('GROV','GRV'),
987     ('GRV','GRV'),
988     ('GTWAY','GTWY'),
989     ('GTWY','GTWY'),
990     ('HARB','HBR'),
991     ('HARBOR','HBR'),
992     ('HARBORS','HBRS'),
993     ('HARBR','HBR'),
994     ('HAVEN','HVN'),
995     ('HAVN','HVN'),
996     ('HBR','HBR'),
997     ('HEIGHT','HTS'),
998     ('HEIGHTS','HTS'),
999     ('HGTS','HTS'),
1000     ('HIGHWAY','HWY'),
1001     ('HIGHWY','HWY'),
1002     ('HILL','HL'),
1003     ('HILLS','HLS'),
1004     ('HIWAY','HWY'),
1005     ('HIWY','HWY'),
1006     ('HL','HL'),
1007     ('HLLW','HOLW'),
1008     ('HLS','HLS'),
1009     ('HOLLOW','HOLW'),
1010     ('HOLLOWS','HOLW'),
1011     ('HOLW','HOLW'),
1012     ('HOLWS','HOLW'),
1013     ('HRBOR','HBR'),
1014     ('HT','HTS'),
1015     ('HTS','HTS'),
1016     ('HVN','HVN'),
1017     ('HWAY','HWY'),
1018     ('HWY','HWY'),
1019     ('INLET','INLT'),
1020     ('INLT','INLT'),
1021     ('IS','IS'),
1022     ('ISLAND','IS'),
1023     ('ISLANDS','ISS'),
1024     ('ISLANDS','SLNDS'),
1025     ('ISLANDS','SS'),
1026     ('ISLE','ISLE'),
1027     ('ISLES','ISLE'),
1028     ('ISLND','IS'),
1029     ('I','SLNDS'),
1030     ('ISS','ISS'),
1031     ('JCTION','JCT'),
1032     ('JCT','JCT'),
1033     ('JCTN','JCT'),
1034     ('JCTNS','JCTS'),
1035     ('JCTS','JCTS'),
1036     ('JUNCTION','JCT'),
1037     ('JUNCTIONS','JCTS'),
1038     ('JUNCTN','JCT'),
1039     ('JUNCTON','JCT'),
1040     ('KEY','KY'),
1041     ('KEYS','KYS'),
1042     ('KNL','KNL'),
1043     ('KNLS','KNLS'),
1044     ('KNOL','KNL'),
1045     ('KNOLL','KNL'),
1046     ('KNOLLS','KNLS'),
1047     ('KY','KY'),
1048     ('KYS','KYS'),
1049     ('LAKE','LK'),
1050     ('LAKES','LKS'),
1051     ('LA','LN'),
1052     ('LANDING','LNDG'),
1053     ('LAND','LAND'),
1054     ('LANE','LN'),
1055     ('LANES','LN'),
1056     ('LCK','LCK'),
1057     ('LCKS','LCKS'),
1058     ('LDGE','LDG'),
1059     ('LDG','LDG'),
1060     ('LF','LF'),
1061     ('LGT','LGT'),
1062     ('LIGHT','LGT'),
1063     ('LIGHTS','LGTS'),
1064     ('LK','LK'),
1065     ('LKS','LKS'),
1066     ('LNDG','LNDG'),
1067     ('LNDNG','LNDG'),
1068     ('LN','LN'),
1069     ('LOAF','LF'),
1070     ('LOCK','LCK'),
1071     ('LOCKS','LCKS'),
1072     ('LODGE','LDG'),
1073     ('LODG','LDG'),
1074     ('LOOP','LOOP'),
1075     ('LOOPS','LOOP'),
1076     ('MALL','MALL'),
1077     ('MANOR','MNR'),
1078     ('MANORS','MNRS'),
1079     ('MDW','MDW'),
1080     ('MDWS','MDWS'),
1081     ('MEADOW','MDW'),
1082     ('MEADOWS','MDWS'),
1083     ('MEDOWS','MDWS'),
1084     ('MEWS','MEWS'),
1085     ('MILL','ML'),
1086     ('MILLS','MLS'),
1087     ('MISSION','MSN'),
1088     ('MISSN','MSN'),
1089     ('ML','ML'),
1090     ('MLS','MLS'),
1091     ('MNR','MNR'),
1092     ('MNRS','MNRS'),
1093     ('MNTAIN','MTN'),
1094     ('MNT','MT'),
1095     ('MNTN','MTN'),
1096     ('MNTNS','MTNS'),
1097     ('MOTORWAY','MTWY'),
1098     ('MOUNTAIN','MTN'),
1099     ('MOUNTAINS','MTNS'),
1100     ('MOUNTIN','MTN'),
1101     ('MOUNT','MT'),
1102     ('MSN','MSN'),
1103     ('MSSN','MSN'),
1104     ('MTIN','MTN'),
1105     ('MT','MT'),
1106     ('MTN','MTN'),
1107     ('NCK','NCK'),
1108     ('NECK','NCK'),
1109     ('ORCHARD','ORCH'),
1110     ('ORCH','ORCH'),
1111     ('ORCHRD','ORCH'),
1112     ('OVAL','OVAL'),
1113     ('OVERPASS','OPAS'),
1114     ('OVL','OVAL'),
1115     ('PARK','PARK'),
1116     ('PARKS','PARK'),
1117     ('PARKWAY','PKWY'),
1118     ('PARKWAYS','PKWY'),
1119     ('PARKWY','PKWY'),
1120     ('PASSAGE','PSGE'),
1121     ('PASS','PASS'),
1122     ('PATH','PATH'),
1123     ('PATHS','PATH'),
1124     ('PIKE','PIKE'),
1125     ('PIKES','PIKE'),
1126     ('PINE','PNE'),
1127     ('PINES','PNES'),
1128     ('PK','PARK'),
1129     ('PKWAY','PKWY'),
1130     ('PKWY','PKWY'),
1131     ('PKWYS','PKWY'),
1132     ('PKY','PKWY'),
1133     ('PLACE','PL'),
1134     ('PLAINES','PLNS'),
1135     ('PLAIN','PLN'),
1136     ('PLAINS','PLNS'),
1137     ('PLAZA','PLZ'),
1138     ('PLN','PLN'),
1139     ('PLNS','PLNS'),
1140     ('PL','PL'),
1141     ('PLZA','PLZ'),
1142     ('PLZ','PLZ'),
1143     ('PNES','PNES'),
1144     ('POINT','PT'),
1145     ('POINTS','PTS'),
1146     ('PORT','PRT'),
1147     ('PORTS','PRTS'),
1148     ('PRAIRIE','PR'),
1149     ('PRARIE','PR'),
1150     ('PRK','PARK'),
1151     ('PR','PR'),
1152     ('PRR','PR'),
1153     ('PRT','PRT'),
1154     ('PRTS','PRTS'),
1155     ('PT','PT'),
1156     ('PTS','PTS'),
1157     ('RADIAL','RADL'),
1158     ('RADIEL','RADL'),
1159     ('RADL','RADL'),
1160     ('RAD','RADL'),
1161     ('RAMP','RAMP'),
1162     ('RANCHES','RNCH'),
1163     ('RANCH','RNCH'),
1164     ('RAPID','RPD'),
1165     ('RAPIDS','RPDS'),
1166     ('RDGE','RDG'),
1167     ('RDG','RDG'),
1168     ('RDGS','RDGS'),
1169     ('RD','RD'),
1170     ('RDS','RDS'),
1171     ('REST','RST'),
1172     ('RIDGE','RDG'),
1173     ('RIDGES','RDGS'),
1174     ('RIVER','RIV'),
1175     ('RIV','RIV'),
1176     ('RIVR','RIV'),
1177     ('RNCH','RNCH'),
1178     ('RNCHS','RNCH'),
1179     ('ROAD','RD'),
1180     ('ROADS','RDS'),
1181     ('ROUTE','RTE'),
1182     ('ROW','ROW'),
1183     ('RPD','RPD'),
1184     ('RPDS','RPDS'),
1185     ('RST','RST'),
1186     ('RUE','RUE'),
1187     ('RUN','RUN'),
1188     ('RVR','RIV'),
1189     ('SHL','SHL'),
1190     ('SHLS','SHLS'),
1191     ('SHOAL','SHL'),
1192     ('SHOALS','SHLS'),
1193     ('SHOAR','SHR'),
1194     ('SHOARS','SHRS'),
1195     ('SHORE','SHR'),
1196     ('SHORES','SHRS'),
1197     ('SHR','SHR'),
1198     ('SHRS','SHRS'),
1199     ('SKYWAY','SKWY'),
1200     ('SMT','SMT'),
1201     ('SPG','SPG'),
1202     ('SPGS','SPGS'),
1203     ('SPNG','SPG'),
1204     ('SPNGS','SPGS'),
1205     ('SPRING','SPG'),
1206     ('SPRINGS','SPGS'),
1207     ('SPRNG','SPG'),
1208     ('SPRNGS','SPGS'),
1209     ('SPUR','SPUR'),
1210     ('SPURS','SPUR'),
1211     ('SQRE','SQ'),
1212     ('SQR','SQ'),
1213     ('SQRS','SQS'),
1214     ('SQ','SQ'),
1215     ('SQUARE','SQ'),
1216     ('SQUARES','SQS'),
1217     ('SQU','SQ'),
1218     ('STA','STA'),
1219     ('STATION','STA'),
1220     ('STATN','STA'),
1221     ('STN','STA'),
1222     ('STRA','STRA'),
1223     ('STRAVEN','STRA'),
1224     ('STRAVENUE','STRA'),
1225     ('STRAVE','STRA'),
1226     ('STRAVN','STRA'),
1227     ('STRAV','STRA'),
1228     ('STREAM','STRM'),
1229     ('STREETS','STS'),
1230     ('STREET','ST'),
1231     ('STREME','STRM'),
1232     ('STRM','STRM'),
1233     ('STR','ST'),
1234     ('STRT','ST'),
1235     ('STRVN','STRA'),
1236     ('STRVNUE','STRA'),
1237     ('ST','ST'),
1238     ('SUMIT','SMT'),
1239     ('SUMITT','SMT'),
1240     ('SUMMIT','SMT'),
1241     ('TERRACE','TER'),
1242     ('TERR','TER'),
1243     ('TER','TER'),
1244     ('THROUGHWAY','TRWY'),
1245     ('TPKE','TPKE'),
1246     ('TPK','TPKE'),
1247     ('TRACES','TRCE'),
1248     ('TRACE','TRCE'),
1249     ('TRACKS','TRAK'),
1250     ('TRACK','TRAK'),
1251     ('TRAFFICWAY','TRFY'),
1252     ('TRAILS','TRL'),
1253     ('TRAIL','TRL'),
1254     ('TRAK','TRAK'),
1255     ('TRCE','TRCE'),
1256     ('TRFY','TRFY'),
1257     ('TRKS','TRAK'),
1258     ('TRK','TRAK'),
1259     ('TRLS','TRL'),
1260     ('TRL','TRL'),
1261     ('TRNPK','TPKE'),
1262     ('TRPK','TPKE'),
1263     ('TR','TRL'),
1264     ('TUNEL','TUNL'),
1265     ('TUNLS','TUNL'),
1266     ('TUNL','TUNL'),
1267     ('TUNNELS','TUNL'),
1268     ('TUNNEL','TUNL'),
1269     ('TUNNL','TUNL'),
1270     ('TURNPIKE','TPKE'),
1271     ('TURNPK','TPKE'),
1272     ('UNDERPASS','UPAS'),
1273     ('UNIONS','UNS'),
1274     ('UNION','UN'),
1275     ('UN','UN'),
1276     ('VALLEYS','VLYS'),
1277     ('VALLEY','VLY'),
1278     ('VALLY','VLY'),
1279     ('VDCT','IA'),
1280     ('VIADCT','VIA'),
1281     ('VIADUCT','IA'),
1282     ('VIADUCT','VIA'),
1283     ('VIA','VIA'),
1284     ('VIEWS','VWS'),
1285     ('VIEW','VW'),
1286     ('VILLAGES','VLGS'),
1287     ('VILLAGE','VLG'),
1288     ('VILLAG','VLG'),
1289     ('VILLE','VL'),
1290     ('VILLG','VLG'),
1291     ('VILLIAGE','VLG'),
1292     ('VILL','VLG'),
1293     ('VISTA','VIS'),
1294     ('VIST','VIS'),
1295     ('VIS','VIS'),
1296     ('VLGS','VLGS'),
1297     ('VLG','VLG'),
1298     ('VLLY','VLY'),
1299     ('VL','VL'),
1300     ('VLYS','VLYS'),
1301     ('VLY','VLY'),
1302     ('VSTA','VIS'),
1303     ('VST','VIS'),
1304     ('VWS','VWS'),
1305     ('VW','VW'),
1306     ('WALKS','WALK'),
1307     ('WALK','WALK'),
1308     ('WALL','WALL'),
1309     ('WAYS','WAYS'),
1310     ('WAY','WAY'),
1311     ('WELLS','WLS'),
1312     ('WELL','WL'),
1313     ('WLS','WLS'),
1314     ('WY','WAY'),
1315     ('XING','XING');
1316
1317 -- this function should get a smaller range of inputs and benefit more from STABLE, hopefully speeding things up
1318 CREATE OR REPLACE FUNCTION migration_tools._normalize_address_suffix (TEXT) RETURNS TEXT AS $$
1319     DECLARE
1320         suffix TEXT := $1;
1321                 _r RECORD;
1322     BEGIN
1323         --RAISE INFO 'suffix = %', suffix;
1324                 FOR _r IN (SELECT * FROM migration_tools.usps_suffixes) LOOP
1325                         suffix := REGEXP_REPLACE( suffix, _r.suffix_from, _r.suffix_to, 'i');
1326                 END LOOP;
1327                 RETURN suffix;
1328     END;
1329 $$ LANGUAGE PLPGSQL STRICT STABLE;
1330
1331 CREATE OR REPLACE FUNCTION migration_tools.normalize_address_suffix (TEXT) RETURNS TEXT AS $$
1332     BEGIN
1333                 RETURN CASE
1334             WHEN $1 ~ '\s\S+$' THEN REGEXP_REPLACE( $1, '^(.*\s)(\S+)$', '\1' ) || migration_tools._normalize_address_suffix( REGEXP_REPLACE( $1, '^(.*\s)(\S+)$', '\2' ) )
1335             ELSE $1
1336         END;
1337     END;
1338 $$ LANGUAGE PLPGSQL STRICT STABLE;
1339
1340 CREATE OR REPLACE FUNCTION migration_tools.rebarcode (o TEXT, t BIGINT) RETURNS TEXT AS $$
1341     DECLARE
1342         n TEXT := o;
1343     BEGIN
1344         IF o ~ E'^\\d+$' AND o !~ E'^0' AND length(o) < 19 THEN -- for reference, the max value for a bigint is 9223372036854775807.  May also want to consider the case where folks want to add prefixes to non-numeric barcodes
1345             IF o::BIGINT < t THEN
1346                 n = o::BIGINT + t;
1347             END IF;
1348         END IF;
1349
1350         RETURN n;
1351     END;
1352 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
1353
1354 CREATE OR REPLACE FUNCTION migration_tools.base_profile_map (TEXT) RETURNS TEXT AS $$
1355     DECLARE
1356         migration_schema ALIAS FOR $1;
1357         output TEXT;
1358     BEGIN
1359         FOR output IN
1360             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_profile_map'';'
1361         LOOP
1362             RETURN output;
1363         END LOOP;
1364     END;
1365 $$ LANGUAGE PLPGSQL STRICT STABLE;
1366
1367 CREATE OR REPLACE FUNCTION migration_tools.base_item_dynamic_field_map (TEXT) RETURNS TEXT AS $$
1368     DECLARE
1369         migration_schema ALIAS FOR $1;
1370         output TEXT;
1371     BEGIN
1372         FOR output IN
1373             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_item_dynamic_field_map'';'
1374         LOOP
1375             RETURN output;
1376         END LOOP;
1377     END;
1378 $$ LANGUAGE PLPGSQL STRICT STABLE;
1379
1380 CREATE OR REPLACE FUNCTION migration_tools.base_copy_location_map (TEXT) RETURNS TEXT AS $$
1381     DECLARE
1382         migration_schema ALIAS FOR $1;
1383         output TEXT;
1384     BEGIN
1385         FOR output IN
1386             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_copy_location_map'';'
1387         LOOP
1388             RETURN output;
1389         END LOOP;
1390     END;
1391 $$ LANGUAGE PLPGSQL STRICT STABLE;
1392
1393 CREATE OR REPLACE FUNCTION migration_tools.base_circ_field_map (TEXT) RETURNS TEXT AS $$
1394     DECLARE
1395         migration_schema ALIAS FOR $1;
1396         output TEXT;
1397     BEGIN
1398         FOR output IN
1399             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_circ_field_map'';'
1400         LOOP
1401             RETURN output;
1402         END LOOP;
1403     END;
1404 $$ LANGUAGE PLPGSQL STRICT STABLE;
1405
1406 CREATE OR REPLACE FUNCTION migration_tools.map_base_patron_profile (TEXT,TEXT,INTEGER) RETURNS VOID AS $$
1407     DECLARE
1408         migration_schema ALIAS FOR $1;
1409         profile_map TEXT;
1410         patron_table ALIAS FOR $2;
1411         default_patron_profile ALIAS FOR $3;
1412         sql TEXT;
1413         sql_update TEXT;
1414         sql_where1 TEXT := '';
1415         sql_where2 TEXT := '';
1416         sql_where3 TEXT := '';
1417         output RECORD;
1418     BEGIN
1419         SELECT migration_tools.base_profile_map(migration_schema) INTO STRICT profile_map;
1420         FOR output IN 
1421             EXECUTE 'SELECT * FROM ' || profile_map || E' ORDER BY id;'
1422         LOOP
1423             sql_update := 'UPDATE ' || patron_table || ' AS u SET profile = perm_grp_id FROM ' || profile_map || ' AS m WHERE ';
1424             sql_where1 := NULLIF(output.legacy_field1,'') || ' = ' || quote_literal( output.legacy_value1 ) || ' AND legacy_field1 = ' || quote_literal(output.legacy_field1) || ' AND legacy_value1 = ' || quote_literal(output.legacy_value1);
1425             sql_where2 := NULLIF(output.legacy_field2,'') || ' = ' || quote_literal( output.legacy_value2 ) || ' AND legacy_field2 = ' || quote_literal(output.legacy_field2) || ' AND legacy_value2 = ' || quote_literal(output.legacy_value2);
1426             sql_where3 := NULLIF(output.legacy_field3,'') || ' = ' || quote_literal( output.legacy_value3 ) || ' AND legacy_field3 = ' || quote_literal(output.legacy_field3) || ' AND legacy_value3 = ' || quote_literal(output.legacy_value3);
1427             sql := sql_update || COALESCE(sql_where1,'') || CASE WHEN sql_where1 <> '' AND sql_where2<> ''  THEN ' AND ' ELSE '' END || COALESCE(sql_where2,'') || CASE WHEN sql_where2 <> '' AND sql_where3 <> '' THEN ' AND ' ELSE '' END || COALESCE(sql_where3,'') || ';';
1428             --RAISE INFO 'sql = %', sql;
1429             PERFORM migration_tools.exec( $1, sql );
1430         END LOOP;
1431         PERFORM migration_tools.exec( $1, 'UPDATE ' || patron_table || ' AS u SET profile = ' || quote_literal(default_patron_profile) || ' WHERE profile IS NULL;'  );
1432         BEGIN
1433             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_patron_mapping_profile'', now() );' );
1434         EXCEPTION
1435             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_patron_mapping_profile'';' );
1436         END;
1437     END;
1438 $$ LANGUAGE PLPGSQL STRICT STABLE;
1439
1440 CREATE OR REPLACE FUNCTION migration_tools.map_base_item_table_dynamic (TEXT,TEXT) RETURNS VOID AS $$
1441     DECLARE
1442         migration_schema ALIAS FOR $1;
1443         field_map TEXT;
1444         item_table ALIAS FOR $2;
1445         sql TEXT;
1446         sql_update TEXT;
1447         sql_where1 TEXT := '';
1448         sql_where2 TEXT := '';
1449         sql_where3 TEXT := '';
1450         output RECORD;
1451     BEGIN
1452         SELECT migration_tools.base_item_dynamic_field_map(migration_schema) INTO STRICT field_map;
1453         FOR output IN 
1454             EXECUTE 'SELECT * FROM ' || field_map || E' ORDER BY id;'
1455         LOOP
1456             sql_update := 'UPDATE ' || item_table || ' AS i SET ' || output.evergreen_field || E' = ' || quote_literal(output.evergreen_value) || '::' || output.evergreen_datatype || E' FROM ' || field_map || ' AS m WHERE ';
1457             sql_where1 := NULLIF(output.legacy_field1,'') || ' = ' || quote_literal( output.legacy_value1 ) || ' AND legacy_field1 = ' || quote_literal(output.legacy_field1) || ' AND legacy_value1 = ' || quote_literal(output.legacy_value1);
1458             sql_where2 := NULLIF(output.legacy_field2,'') || ' = ' || quote_literal( output.legacy_value2 ) || ' AND legacy_field2 = ' || quote_literal(output.legacy_field2) || ' AND legacy_value2 = ' || quote_literal(output.legacy_value2);
1459             sql_where3 := NULLIF(output.legacy_field3,'') || ' = ' || quote_literal( output.legacy_value3 ) || ' AND legacy_field3 = ' || quote_literal(output.legacy_field3) || ' AND legacy_value3 = ' || quote_literal(output.legacy_value3);
1460             sql := sql_update || COALESCE(sql_where1,'') || CASE WHEN sql_where1 <> '' AND sql_where2<> ''  THEN ' AND ' ELSE '' END || COALESCE(sql_where2,'') || CASE WHEN sql_where2 <> '' AND sql_where3 <> '' THEN ' AND ' ELSE '' END || COALESCE(sql_where3,'') || ';';
1461             --RAISE INFO 'sql = %', sql;
1462             PERFORM migration_tools.exec( $1, sql );
1463         END LOOP;
1464         BEGIN
1465             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_item_mapping_dynamic'', now() );' );
1466         EXCEPTION
1467             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_item_mapping_dynamic'';' );
1468         END;
1469     END;
1470 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1471
1472 CREATE OR REPLACE FUNCTION migration_tools.map_base_item_table_locations (TEXT,TEXT) RETURNS VOID AS $$
1473     DECLARE
1474         migration_schema ALIAS FOR $1;
1475         base_copy_location_map TEXT;
1476         item_table ALIAS FOR $2;
1477         sql TEXT;
1478         sql_update TEXT;
1479         sql_where1 TEXT := '';
1480         sql_where2 TEXT := '';
1481         sql_where3 TEXT := '';
1482         output RECORD;
1483     BEGIN
1484         SELECT migration_tools.base_copy_location_map(migration_schema) INTO STRICT base_copy_location_map;
1485         FOR output IN 
1486             EXECUTE 'SELECT * FROM ' || base_copy_location_map || E' ORDER BY id;'
1487         LOOP
1488             sql_update := 'UPDATE ' || item_table || ' AS i SET location = m.location FROM ' || base_copy_location_map || ' AS m WHERE ';
1489             sql_where1 := NULLIF(output.legacy_field1,'') || ' = ' || quote_literal( output.legacy_value1 ) || ' AND legacy_field1 = ' || quote_literal(output.legacy_field1) || ' AND legacy_value1 = ' || quote_literal(output.legacy_value1);
1490             sql_where2 := NULLIF(output.legacy_field2,'') || ' = ' || quote_literal( output.legacy_value2 ) || ' AND legacy_field2 = ' || quote_literal(output.legacy_field2) || ' AND legacy_value2 = ' || quote_literal(output.legacy_value2);
1491             sql_where3 := NULLIF(output.legacy_field3,'') || ' = ' || quote_literal( output.legacy_value3 ) || ' AND legacy_field3 = ' || quote_literal(output.legacy_field3) || ' AND legacy_value3 = ' || quote_literal(output.legacy_value3);
1492             sql := sql_update || COALESCE(sql_where1,'') || CASE WHEN sql_where1 <> '' AND sql_where2<> ''  THEN ' AND ' ELSE '' END || COALESCE(sql_where2,'') || CASE WHEN sql_where2 <> '' AND sql_where3 <> '' THEN ' AND ' ELSE '' END || COALESCE(sql_where3,'') || ';';
1493             --RAISE INFO 'sql = %', sql;
1494             PERFORM migration_tools.exec( $1, sql );
1495         END LOOP;
1496         BEGIN
1497             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_item_mapping_locations'', now() );' );
1498         EXCEPTION
1499             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_item_mapping_locations'';' );
1500         END;
1501     END;
1502 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1503
1504 -- circulate       loan period     max renewals    max out fine amount     fine interval   max fine        item field 1    item value 1    item field 2    item value 2    patron field 1  patron value 1  patron field 2  patron value 2
1505 CREATE OR REPLACE FUNCTION migration_tools.map_base_circ_table_dynamic (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
1506     DECLARE
1507         migration_schema ALIAS FOR $1;
1508         field_map TEXT;
1509         circ_table ALIAS FOR $2;
1510         item_table ALIAS FOR $3;
1511         patron_table ALIAS FOR $4;
1512         sql TEXT;
1513         sql_update TEXT;
1514         sql_where1 TEXT := '';
1515         sql_where2 TEXT := '';
1516         sql_where3 TEXT := '';
1517         sql_where4 TEXT := '';
1518         output RECORD;
1519     BEGIN
1520         SELECT migration_tools.base_circ_field_map(migration_schema) INTO STRICT field_map;
1521         FOR output IN 
1522             EXECUTE 'SELECT * FROM ' || field_map || E' ORDER BY id;'
1523         LOOP
1524             sql_update := 'UPDATE ' || circ_table || ' AS c SET duration = ' || quote_literal(output.loan_period) || '::INTERVAL, renewal_remaining = ' || quote_literal(output.max_renewals) || '::INTEGER, recuring_fine = ' || quote_literal(output.fine_amount) || '::NUMERIC(6,2), fine_interval = ' || quote_literal(output.fine_interval) || '::INTERVAL, max_fine = ' || quote_literal(output.max_fine) || '::NUMERIC(6,2) FROM ' || field_map || ' AS m, ' || item_table || ' AS i, ' || patron_table || ' AS u WHERE c.usr = u.id AND c.target_copy = i.id AND ';
1525             sql_where1 := NULLIF(output.item_field1,'') || ' = ' || quote_literal( output.item_value1 ) || ' AND item_field1 = ' || quote_literal(output.item_field1) || ' AND item_value1 = ' || quote_literal(output.item_value1);
1526             sql_where2 := NULLIF(output.item_field2,'') || ' = ' || quote_literal( output.item_value2 ) || ' AND item_field2 = ' || quote_literal(output.item_field2) || ' AND item_value2 = ' || quote_literal(output.item_value2);
1527             sql_where3 := NULLIF(output.patron_field1,'') || ' = ' || quote_literal( output.patron_value1 ) || ' AND patron_field1 = ' || quote_literal(output.patron_field1) || ' AND patron_value1 = ' || quote_literal(output.patron_value1);
1528             sql_where4 := NULLIF(output.patron_field2,'') || ' = ' || quote_literal( output.patron_value2 ) || ' AND patron_field2 = ' || quote_literal(output.patron_field2) || ' AND patron_value2 = ' || quote_literal(output.patron_value2);
1529             sql := sql_update || COALESCE(sql_where1,'') || CASE WHEN sql_where1 <> '' AND sql_where2<> ''  THEN ' AND ' ELSE '' END || COALESCE(sql_where2,'') || CASE WHEN sql_where2 <> '' AND sql_where3 <> '' THEN ' AND ' ELSE '' END || COALESCE(sql_where3,'') || CASE WHEN sql_where3 <> '' AND sql_where4 <> '' THEN ' AND ' ELSE '' END || COALESCE(sql_where4,'') || ';';
1530             --RAISE INFO 'sql = %', sql;
1531             PERFORM migration_tools.exec( $1, sql );
1532         END LOOP;
1533         BEGIN
1534             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_circ_field_mapping'', now() );' );
1535         EXCEPTION
1536             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_circ_field_mapping'';' );
1537         END;
1538     END;
1539 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
1540
1541 -- expand_barcode
1542 --   $barcode      source barcode
1543 --   $prefix       prefix to add to barcode, NULL = add no prefix
1544 --   $maxlen       maximum length of barcode; default to 14 if left NULL
1545 --   $pad          padding string to apply to left of source barcode before adding
1546 --                 prefix and suffix; set to NULL or '' if no padding is desired
1547 --   $suffix       suffix to add to barcode, NULL = add no suffix
1548 --
1549 -- Returns a new string consisting of prefix concatenated with padded barcode and suffix.
1550 -- If new barcode would be longer than $maxlen, the original barcode is returned instead.
1551 --
1552 CREATE OR REPLACE FUNCTION migration_tools.expand_barcode (TEXT, TEXT, INTEGER, TEXT, TEXT) RETURNS TEXT AS $$
1553     my ($barcode, $prefix, $maxlen, $pad, $suffix) = @_;
1554
1555     # default case
1556     return unless defined $barcode;
1557
1558     $prefix     = '' unless defined $prefix;
1559     $maxlen ||= 14;
1560     $pad        = '0' unless defined $pad;
1561     $suffix     = '' unless defined $suffix;
1562
1563     # bail out if adding prefix and suffix would bring new barcode over max length
1564     return $barcode if (length($prefix) + length($barcode) + length($suffix)) > $maxlen;
1565
1566     my $new_barcode = $barcode;
1567     if ($pad ne '') {
1568         my $pad_length = $maxlen - length($prefix) - length($suffix);
1569         if (length($barcode) < $pad_length) {
1570             # assuming we always want padding on the left
1571             # also assuming that it is possible to have the pad string be longer than 1 character
1572             $new_barcode = substr($pad x ($pad_length - length($barcode)), 0, $pad_length - length($barcode)) . $new_barcode;
1573         }
1574     }
1575
1576     # bail out if adding prefix and suffix would bring new barcode over max length
1577     return $barcode if (length($prefix) + length($new_barcode) + length($suffix)) > $maxlen;
1578
1579     return "$prefix$new_barcode$suffix";
1580 $$ LANGUAGE PLPERLU STABLE;
1581
1582 -- remove previous version of this function
1583 DROP FUNCTION IF EXISTS migration_tools.attempt_cast(TEXT, TEXT, TEXT);
1584
1585 CREATE OR REPLACE FUNCTION migration_tools.attempt_cast (TEXT, TEXT) RETURNS TEXT AS $$
1586     DECLARE
1587         attempt_value ALIAS FOR $1;
1588         datatype ALIAS FOR $2;
1589     BEGIN
1590         EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::' || datatype || ' AS a;';
1591         RETURN attempt_value;
1592     EXCEPTION
1593         WHEN OTHERS THEN RETURN NULL;
1594     END;
1595 $$ LANGUAGE PLPGSQL STRICT STABLE;
1596
1597 CREATE OR REPLACE FUNCTION migration_tools.attempt_date (TEXT,TEXT) RETURNS DATE AS $$
1598     DECLARE
1599         attempt_value ALIAS FOR $1;
1600         fail_value ALIAS FOR $2;
1601         output DATE;
1602     BEGIN
1603         FOR output IN
1604             EXECUTE 'SELECT ' || quote_literal(REGEXP_REPLACE(attempt_value,'^(\d\d)(\d\d)(\d\d)$','\1-\2-\3')) || '::date AS a;'
1605         LOOP
1606             RETURN output;
1607         END LOOP;
1608     EXCEPTION
1609         WHEN OTHERS THEN
1610             FOR output IN
1611                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::date AS a;'
1612             LOOP
1613                 RETURN output;
1614             END LOOP;
1615     END;
1616 $$ LANGUAGE PLPGSQL STRICT STABLE;
1617
1618 CREATE OR REPLACE FUNCTION migration_tools.attempt_timestamptz (TEXT,TEXT) RETURNS TIMESTAMPTZ AS $$
1619     DECLARE
1620         attempt_value ALIAS FOR $1;
1621         fail_value ALIAS FOR $2;
1622         output TIMESTAMPTZ;
1623     BEGIN
1624         FOR output IN
1625             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::TIMESTAMPTZ AS a;'
1626         LOOP
1627             RETURN output;
1628         END LOOP;
1629     EXCEPTION
1630         WHEN OTHERS THEN
1631             FOR output IN
1632                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::TIMESTAMPTZ AS a;'
1633             LOOP
1634                 RETURN output;
1635             END LOOP;
1636     END;
1637 $$ LANGUAGE PLPGSQL STRICT STABLE;
1638
1639 CREATE OR REPLACE FUNCTION migration_tools.attempt_hz_date (TEXT,TEXT) RETURNS DATE AS $$
1640     DECLARE
1641         attempt_value ALIAS FOR $1;
1642         fail_value ALIAS FOR $2;
1643         output DATE;
1644     BEGIN
1645         FOR output IN
1646             EXECUTE E'SELECT (\'1970-01-01\'::date + \'' || attempt_value || E' days\'::interval)::date AS a;'
1647         LOOP
1648             RETURN output;
1649         END LOOP;
1650     EXCEPTION
1651         WHEN OTHERS THEN
1652             FOR output IN
1653                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::date AS a;'
1654             LOOP
1655                 RETURN output;
1656             END LOOP;
1657     END;
1658 $$ LANGUAGE PLPGSQL STRICT STABLE;
1659
1660 CREATE OR REPLACE FUNCTION migration_tools.attempt_sierra_timestamp (TEXT,TEXT) RETURNS TIMESTAMP AS $$
1661     DECLARE
1662         attempt_value ALIAS FOR $1;
1663         fail_value ALIAS FOR $2;
1664         output TIMESTAMP;
1665     BEGIN
1666             output := REGEXP_REPLACE(attempt_value,E'^(..)(..)(..)(..)(..)$',E'20\\1-\\2-\\3 \\4:\\5')::TIMESTAMP;
1667             RETURN output;
1668     EXCEPTION
1669         WHEN OTHERS THEN
1670             FOR output IN
1671                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::TIMESTAMP AS a;'
1672             LOOP
1673                 RETURN output;
1674             END LOOP;
1675     END;
1676 $$ LANGUAGE PLPGSQL STRICT STABLE;
1677
1678 CREATE OR REPLACE FUNCTION migration_tools.openbiblio2marc (x_bibid TEXT) RETURNS TEXT AS $func$
1679 BEGIN
1680 -- Expects the following table/columns:
1681
1682 -- export_biblio_tsv:
1683 -- l_bibid               | 1
1684 -- l_create_dt           | 2007-03-07 09:03:09
1685 -- l_last_change_dt      | 2015-01-23 11:18:54
1686 -- l_last_change_userid  | 2
1687 -- l_material_cd         | 10
1688 -- l_collection_cd       | 13
1689 -- l_call_nmbr1          | Canada
1690 -- l_call_nmbr2          | ON
1691 -- l_call_nmbr3          | Ottawa 18
1692 -- l_title               | Art and the courts : France ad England
1693 -- l_title_remainder     | from 1259-1328
1694 -- l_responsibility_stmt |
1695 -- l_author              | National Gallery of Canada
1696 -- l_topic1              |
1697 -- l_topic2              |
1698 -- l_topic3              |
1699 -- l_topic4              |
1700 -- l_topic5              |
1701 -- l_opac_flg            | Y
1702 -- l_flag_attention      | 0
1703
1704 -- export_biblio_field_tsv:
1705 -- l_bibid       | 1
1706 -- l_fieldid     | 1
1707 -- l_tag         | 720
1708 -- l_ind1_cd     | N
1709 -- l_ind2_cd     | N
1710 -- l_subfield_cd | a
1711 -- l_field_data  | Brieger, Peter Henry
1712
1713 -- Map export_biblio_tsv as follows:
1714 -- l_call_nmbr?             -> 099a
1715 -- l_author                 -> 100a
1716 -- l_title                  -> 245a
1717 -- l_title_remainder        -> 245b
1718 -- l_responsibility_stmt    -> 245c
1719 -- l_topic?                 -> 650a
1720 -- l_bibid                  -> 001
1721
1722 RETURN
1723     migration_tools.consolidate_tag( migration_tools.make_stub_bib(y.tag,y.ind1,y.ind2,y.data), '245' )
1724 FROM (
1725     select
1726         array_agg(lpad(l_tag,3,'0') || l_subfield_cd) as "tag",
1727         array_agg(l_ind1_cd) as "ind1",
1728         array_agg(l_ind2_cd) as "ind2",
1729         array_agg(l_field_data) as "data"
1730     from (
1731         select
1732             l_tag,
1733             l_subfield_cd,
1734             l_ind1_cd,
1735             l_ind2_cd,
1736             l_field_data
1737         from export_biblio_field_tsv
1738         where l_bibid = x_bibid
1739     union
1740         select
1741             '099' as "l_tag",
1742             'a' as "l_subfield_cd",
1743             ' ' as "l_ind1_cd",
1744             ' ' as "l_ind2_cd",
1745             concat_ws(' ',
1746                 nullif(btrim(l_call_nmbr1),''),
1747                 nullif(btrim(l_call_nmbr2),''),
1748                 nullif(btrim(l_call_nmbr3),'')
1749             ) as "l_field_data"
1750         from export_biblio_tsv
1751         where l_bibid = x_bibid
1752     union
1753         select
1754             '100' as "l_tag",
1755             'a' as "l_subfield_cd",
1756             ' ' as "l_ind1_cd",
1757             ' ' as "l_ind2_cd",
1758             l_author as "l_field_data"
1759         from export_biblio_tsv
1760         where l_bibid = x_bibid and nullif(btrim(l_author),'') is not null
1761     union
1762         select
1763             '245' as "l_tag",
1764             'a' as "l_subfield_cd",
1765             ' ' as "l_ind1_cd",
1766             ' ' as "l_ind2_cd",
1767             l_title as "l_field_data"
1768         from export_biblio_tsv
1769         where l_bibid = x_bibid and nullif(btrim(l_title),'') is not null
1770     union
1771         select
1772             '245' as "l_tag",
1773             'b' as "l_subfield_cd",
1774             ' ' as "l_ind1_cd",
1775             ' ' as "l_ind2_cd",
1776             l_title_remainder as "l_field_data"
1777         from export_biblio_tsv
1778         where l_bibid = x_bibid and nullif(btrim(l_title_remainder),'') is not null
1779     union
1780         select
1781             '650' as "l_tag",
1782             'a' as "l_subfield_cd",
1783             ' ' as "l_ind1_cd",
1784             ' ' as "l_ind2_cd",
1785             l_topic1 as "l_field_data"
1786         from export_biblio_tsv
1787         where l_bibid = x_bibid and nullif(btrim(l_topic1),'') is not null
1788     union
1789         select
1790             '650' as "l_tag",
1791             'a' as "l_subfield_cd",
1792             ' ' as "l_ind1_cd",
1793             ' ' as "l_ind2_cd",
1794             l_topic2 as "l_field_data"
1795         from export_biblio_tsv
1796         where l_bibid = x_bibid and nullif(btrim(l_topic2),'') is not null
1797     union
1798         select
1799             '650' as "l_tag",
1800             'a' as "l_subfield_cd",
1801             ' ' as "l_ind1_cd",
1802             ' ' as "l_ind2_cd",
1803             l_topic3 as "l_field_data"
1804         from export_biblio_tsv
1805         where l_bibid = x_bibid and nullif(btrim(l_topic3),'') is not null
1806     union
1807         select
1808             '650' as "l_tag",
1809             'a' as "l_subfield_cd",
1810             ' ' as "l_ind1_cd",
1811             ' ' as "l_ind2_cd",
1812             l_topic4 as "l_field_data"
1813         from export_biblio_tsv
1814         where l_bibid = x_bibid and nullif(btrim(l_topic4),'') is not null
1815     union
1816         select
1817             '650' as "l_tag",
1818             'a' as "l_subfield_cd",
1819             ' ' as "l_ind1_cd",
1820             ' ' as "l_ind2_cd",
1821             l_topic5 as "l_field_data"
1822         from export_biblio_tsv
1823         where l_bibid = x_bibid and nullif(btrim(l_topic5),'') is not null
1824     union
1825         select
1826             '001' as "l_tag",
1827             '' as "l_subfield_cd",
1828             '' as "l_ind1_cd",
1829             '' as "l_ind2_cd",
1830             l_bibid as "l_field_data"
1831         from export_biblio_tsv
1832         where l_bibid = x_bibid
1833     ) x
1834 ) y;
1835
1836 END
1837 $func$ LANGUAGE plpgsql;
1838
1839 -- add koha holding tag to marc
1840 DROP FUNCTION IF EXISTS migration_tools.generate_koha_holding_tag(TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT);
1841
1842 CREATE OR REPLACE FUNCTION migration_tools.generate_koha_holding_tag(marc TEXT, tag TEXT, ind1 TEXT, ind2 TEXT, barcode TEXT, dateaccessioned TEXT, booksellerid TEXT, homebranch TEXT, price TEXT, replacementprice TEXT, replacementpricedate TEXT, datelastborrowed TEXT, datelastseen TEXT, stack TEXT, notforloan TEXT, damaged TEXT, itemlost TEXT, wthdrawn TEXT, itemcallnumber TEXT, issues TEXT, renewals TEXT, reserves TEXT, restricted TEXT, internalnotes TEXT, itemnotes TEXT, holdingbranch TEXT, location TEXT, onloan TEXT, cn_source TEXT, cn_sort TEXT, ccode TEXT, materials TEXT, uri TEXT, itype TEXT, enumchron TEXT, copynumber TEXT, stocknumber TEXT)
1843  RETURNS TEXT
1844  LANGUAGE plperlu
1845 AS $function$
1846 use strict;
1847 use warnings;
1848
1849 use MARC::Record;
1850 use MARC::File::XML (BinaryEncoding => 'utf8');
1851
1852 binmode(STDERR, ':bytes');
1853 binmode(STDOUT, ':utf8');
1854 binmode(STDERR, ':utf8');
1855
1856 my ($marc_xml, $tag , $ind1 , $ind2 , $barcode , $dateaccessioned , $booksellerid , $homebranch , $price , $replacementprice , $replacementpricedate , $datelastborrowed , $datelastseen , $stack , $notforloan , $damaged , $itemlost , $wthdrawn , $itemcallnumber , $issues , $renewals , $reserves , $restricted , $internalnotes , $itemnotes , $holdingbranch , $location , $onloan , $cn_source , $cn_sort , $ccode , $materials , $uri , $itype , $enumchron , $copynumber , $stocknumber ) = @_;
1857
1858 $marc_xml =~ s/(<leader>.........)./${1}a/;
1859
1860 eval {
1861     $marc_xml = MARC::Record->new_from_xml($marc_xml);
1862 };
1863 if ($@) {
1864     #elog("could not parse $bibid: $@\n");
1865     import MARC::File::XML (BinaryEncoding => 'utf8');
1866     return $marc_xml;
1867 }
1868
1869 my $new_field = new MARC::Field(
1870     $tag, $ind1, $ind2,
1871     'a' => $homebranch,
1872     'b' => $holdingbranch,
1873     'c' => $location,
1874     'p' => $barcode,
1875     'y' => $itype
1876 );
1877
1878 if ($dateaccessioned) { $new_field->add_subfields('d' => $dateaccessioned); }
1879 if ($booksellerid) { $new_field->add_subfields('e' => $booksellerid); }
1880 if ($price) { $new_field->add_subfields('g' => $price); }
1881 if ($replacementprice) { $new_field->add_subfields('v' => $replacementprice); }
1882 if ($replacementpricedate) { $new_field->add_subfields('w' => $replacementpricedate); }
1883 if ($datelastborrowed) { $new_field->add_subfields('s' => $datelastborrowed); }
1884 if ($datelastseen) { $new_field->add_subfields('r' => $datelastseen); }
1885 if ($stack) { $new_field->add_subfields('j' => $stack); }
1886 if ($notforloan) { $new_field->add_subfields('7' => $notforloan); }
1887 if ($damaged) { $new_field->add_subfields('4' => $damaged); }
1888 if ($itemlost) { $new_field->add_subfields('1' => $itemlost); }
1889 if ($wthdrawn) { $new_field->add_subfields('0' => $wthdrawn); }
1890 if ($itemcallnumber) { $new_field->add_subfields('o' => $itemcallnumber); }
1891 if ($issues) { $new_field->add_subfields('l' => $issues); }
1892 if ($renewals) { $new_field->add_subfields('m' => $renewals); }
1893 if ($reserves) { $new_field->add_subfields('n' => $reserves); }
1894 if ($restricted) { $new_field->add_subfields('5' => $restricted); }
1895 if ($internalnotes) { $new_field->add_subfields('x' => $internalnotes); }
1896 if ($itemnotes) { $new_field->add_subfields('z' => $itemnotes); }
1897 if ($onloan) { $new_field->add_subfields('q' => $onloan); }
1898 if ($cn_source) { $new_field->add_subfields('2' => $cn_source); }
1899 if ($cn_sort) { $new_field->add_subfields('6' => $cn_sort); }
1900 if ($ccode) { $new_field->add_subfields('8' => $ccode); }
1901 if ($materials) { $new_field->add_subfields('3' => $materials); }
1902 if ($uri) { $new_field->add_subfields('u' => $uri); }
1903 if ($enumchron) { $new_field->add_subfields('h' => $enumchron); }
1904 if ($copynumber) { $new_field->add_subfields('t' => $copynumber); }
1905 if ($stocknumber) { $new_field->add_subfields('i' => $stocknumber); }
1906
1907 $marc_xml->insert_grouped_field( $new_field );
1908
1909 return $marc_xml->as_xml_record();
1910
1911 $function$;
1912
1913 CREATE OR REPLACE FUNCTION migration_tools.attempt_money (TEXT,TEXT) RETURNS NUMERIC(8,2) AS $$
1914     DECLARE
1915         attempt_value ALIAS FOR $1;
1916         fail_value ALIAS FOR $2;
1917         output NUMERIC(8,2);
1918     BEGIN
1919         FOR output IN
1920             EXECUTE 'SELECT ' || quote_literal(REPLACE(REPLACE(attempt_value,'$',''),',','')) || '::NUMERIC(8,2) AS a;'
1921         LOOP
1922             RETURN output;
1923         END LOOP;
1924     EXCEPTION
1925         WHEN OTHERS THEN
1926             FOR output IN
1927                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(8,2) AS a;'
1928             LOOP
1929                 RETURN output;
1930             END LOOP;
1931     END;
1932 $$ LANGUAGE PLPGSQL STRICT STABLE;
1933
1934 CREATE OR REPLACE FUNCTION migration_tools.attempt_money6 (TEXT,TEXT) RETURNS NUMERIC(6,2) AS $$
1935     DECLARE
1936         attempt_value ALIAS FOR $1;
1937         fail_value ALIAS FOR $2;
1938         output NUMERIC(6,2);
1939     BEGIN
1940         FOR output IN
1941             EXECUTE 'SELECT ' || quote_literal(REPLACE(REPLACE(attempt_value,'$',''),',','')) || '::NUMERIC(6,2) AS a;'
1942         LOOP
1943             RETURN output;
1944         END LOOP;
1945     EXCEPTION
1946         WHEN OTHERS THEN
1947             FOR output IN
1948                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(6,2) AS a;'
1949             LOOP
1950                 RETURN output;
1951             END LOOP;
1952     END;
1953 $$ LANGUAGE PLPGSQL STRICT STABLE;
1954
1955 CREATE OR REPLACE FUNCTION migration_tools.attempt_money_from_pennies (TEXT,TEXT) RETURNS NUMERIC(8,2) AS $$
1956     DECLARE
1957         attempt_value ALIAS FOR $1;
1958         fail_value ALIAS FOR $2;
1959         output NUMERIC(8,2);
1960     BEGIN
1961         IF length(regexp_replace(attempt_value,'^0+','')) > 10 THEN
1962             RAISE EXCEPTION 'too many digits';
1963         END IF;
1964         FOR output IN
1965             EXECUTE 'SELECT ' || quote_literal((left(lpad(regexp_replace(attempt_value,'^0+',''),10,'0'),-2) || '.' || right(lpad(regexp_replace(attempt_value,'^0+',''),10,'0'),2))::numeric(8,2)) || '::NUMERIC(8,2) AS a;'
1966         LOOP
1967             RETURN output;
1968         END LOOP;
1969     EXCEPTION
1970         WHEN OTHERS THEN
1971             FOR output IN
1972                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(8,2) AS a;'
1973             LOOP
1974                 RETURN output;
1975             END LOOP;
1976     END;
1977 $$ LANGUAGE PLPGSQL STRICT STABLE;
1978
1979 CREATE OR REPLACE FUNCTION migration_tools.attempt_money_from_pennies6 (TEXT,TEXT) RETURNS NUMERIC(6,2) AS $$
1980     DECLARE
1981         attempt_value ALIAS FOR $1;
1982         fail_value ALIAS FOR $2;
1983         output NUMERIC(6,2);
1984     BEGIN
1985         IF length(regexp_replace(attempt_value,'^0+','')) > 8 THEN
1986             RAISE EXCEPTION 'too many digits';
1987         END IF;
1988         FOR output IN
1989             EXECUTE 'SELECT ' || quote_literal((left(lpad(regexp_replace(attempt_value,'^0+',''),8,'0'),-2) || '.' || right(lpad(regexp_replace(attempt_value,'^0+',''),8,'0'),2))::numeric(6,2)) || '::NUMERIC(6,2) AS a;'
1990         LOOP
1991             RETURN output;
1992         END LOOP;
1993     EXCEPTION
1994         WHEN OTHERS THEN
1995             FOR output IN
1996                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(6,2) AS a;'
1997             LOOP
1998                 RETURN output;
1999             END LOOP;
2000     END;
2001 $$ LANGUAGE PLPGSQL STRICT STABLE;
2002
2003 -- add_codabar_checkdigit
2004 --   $barcode      source barcode
2005 --
2006 -- If the source string is 13 or 14 characters long and contains only digits, adds or replaces the 14
2007 -- character with a checkdigit computed according to the usual algorithm for library barcodes
2008 -- using the Codabar symbology - see <http://www.makebarcode.com/specs/codabar.html>.  If the
2009 -- input string does not meet those requirements, it is returned unchanged.
2010 --
2011 CREATE OR REPLACE FUNCTION migration_tools.add_codabar_checkdigit (TEXT) RETURNS TEXT AS $$
2012     my $barcode = shift;
2013
2014     return $barcode if $barcode !~ /^\d{13,14}$/;
2015     $barcode = substr($barcode, 0, 13); # ignore 14th digit
2016     my @digits = split //, $barcode;
2017     my $total = 0;
2018     $total += $digits[$_] foreach (1, 3, 5, 7, 9, 11);
2019     $total += (2 * $digits[$_] >= 10) ? (2 * $digits[$_] - 9) : (2 * $digits[$_]) foreach (0, 2, 4, 6, 8, 10, 12);
2020     my $remainder = $total % 10;
2021     my $checkdigit = ($remainder == 0) ? $remainder : 10 - $remainder;
2022     return $barcode . $checkdigit; 
2023 $$ LANGUAGE PLPERLU STRICT STABLE;
2024
2025 -- add_code39mod43_checkdigit
2026 --   $barcode      source barcode
2027 --
2028 -- If the source string is 13 or 14 characters long and contains only valid
2029 -- Code 39 mod 43 characters, adds or replaces the 14th
2030 -- character with a checkdigit computed according to the usual algorithm for library barcodes
2031 -- using the Code 39 mod 43 symbology - see <http://en.wikipedia.org/wiki/Code_39#Code_39_mod_43>.  If the
2032 -- input string does not meet those requirements, it is returned unchanged.
2033 --
2034 CREATE OR REPLACE FUNCTION migration_tools.add_code39mod43_checkdigit (TEXT) RETURNS TEXT AS $$
2035     my $barcode = shift;
2036
2037     return $barcode if $barcode !~ /^[0-9A-Z. $\/+%-]{13,14}$/;
2038     $barcode = substr($barcode, 0, 13); # ignore 14th character
2039
2040     my @valid_chars = split //, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%';
2041     my %nums = map { $valid_chars[$_] => $_ } (0..42);
2042
2043     my $total = 0;
2044     $total += $nums{$_} foreach split(//, $barcode);
2045     my $remainder = $total % 43;
2046     my $checkdigit = $valid_chars[$remainder];
2047     return $barcode . $checkdigit;
2048 $$ LANGUAGE PLPERLU STRICT STABLE;
2049
2050 -- add_mod16_checkdigit
2051 --   $barcode      source barcode
2052 --
2053 -- https://www.activebarcode.com/codes/checkdigit/modulo16.html
2054
2055 CREATE OR REPLACE FUNCTION migration_tools.add_mod16_checkdigit (TEXT) RETURNS TEXT AS $$
2056     my $barcode = shift;
2057
2058     my @digits = split //, $barcode;
2059     my $total = 0;
2060     foreach $digit (@digits) {
2061         if ($digit =~ /[0-9]/) { $total += $digit;
2062         } elsif ($digit eq '-') { $total += 10;
2063         } elsif ($digit eq '$') { $total += 11;
2064         } elsif ($digit eq ':') { $total += 12;
2065         } elsif ($digit eq '/') { $total += 13;
2066         } elsif ($digit eq '.') { $total += 14;
2067         } elsif ($digit eq '+') { $total += 15;
2068         } elsif ($digit eq 'A') { $total += 16;
2069         } elsif ($digit eq 'B') { $total += 17;
2070         } elsif ($digit eq 'C') { $total += 18;
2071         } elsif ($digit eq 'D') { $total += 19;
2072         } else { die "invalid digit <$digit>";
2073         }
2074     }
2075     my $remainder = $total % 16;
2076     my $difference = 16 - $remainder;
2077     my $checkdigit;
2078     if ($difference < 10) { $checkdigit = $difference;
2079     } elsif ($difference == 10) { $checkdigit = '-';
2080     } elsif ($difference == 11) { $checkdigit = '$';
2081     } elsif ($difference == 12) { $checkdigit = ':';
2082     } elsif ($difference == 13) { $checkdigit = '/';
2083     } elsif ($difference == 14) { $checkdigit = '.';
2084     } elsif ($difference == 15) { $checkdigit = '+';
2085     } else { die "error calculating checkdigit";
2086     }
2087
2088     return $barcode . $checkdigit;
2089 $$ LANGUAGE PLPERLU STRICT STABLE;
2090
2091 CREATE OR REPLACE FUNCTION migration_tools.attempt_phone (TEXT,TEXT) RETURNS TEXT AS $$
2092   DECLARE
2093     phone TEXT := $1;
2094     areacode TEXT := $2;
2095     temp TEXT := '';
2096     output TEXT := '';
2097     n_digits INTEGER := 0;
2098   BEGIN
2099     temp := phone;
2100     temp := REGEXP_REPLACE(temp, '^1*[^0-9]*(?=[0-9])', '');
2101     temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2-\\3');
2102     n_digits := LENGTH(REGEXP_REPLACE(REGEXP_REPLACE(temp, '(.*)?[a-zA-Z].*', E'\\1') , '[^0-9]', '', 'g'));
2103     IF n_digits = 7 AND areacode <> '' THEN
2104       temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2');
2105       output := (areacode || '-' || temp);
2106     ELSE
2107       output := temp;
2108     END IF;
2109     RETURN output;
2110   END;
2111
2112 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
2113
2114 CREATE OR REPLACE FUNCTION migration_tools.set_leader (TEXT, INT, TEXT) RETURNS TEXT AS $$
2115   my ($marcxml, $pos, $value) = @_;
2116
2117   use MARC::Record;
2118   use MARC::File::XML;
2119
2120   my $xml = $marcxml;
2121   eval {
2122     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
2123     my $leader = $marc->leader();
2124     substr($leader, $pos, 1) = $value;
2125     $marc->leader($leader);
2126     $xml = $marc->as_xml_record;
2127     $xml =~ s/^<\?.+?\?>$//mo;
2128     $xml =~ s/\n//sgo;
2129     $xml =~ s/>\s+</></sgo;
2130   };
2131   return $xml;
2132 $$ LANGUAGE PLPERLU STABLE;
2133
2134 CREATE OR REPLACE FUNCTION migration_tools.set_008 (TEXT, INT, TEXT) RETURNS TEXT AS $$
2135   my ($marcxml, $pos, $value) = @_;
2136
2137   use MARC::Record;
2138   use MARC::File::XML;
2139
2140   my $xml = $marcxml;
2141   eval {
2142     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
2143     my $f008 = $marc->field('008');
2144
2145     if ($f008) {
2146        my $field = $f008->data();
2147        substr($field, $pos, 1) = $value;
2148        $f008->update($field);
2149        $xml = $marc->as_xml_record;
2150        $xml =~ s/^<\?.+?\?>$//mo;
2151        $xml =~ s/\n//sgo;
2152        $xml =~ s/>\s+</></sgo;
2153     }
2154   };
2155   return $xml;
2156 $$ LANGUAGE PLPERLU STABLE;
2157
2158
2159 CREATE OR REPLACE FUNCTION migration_tools.is_staff_profile (INT) RETURNS BOOLEAN AS $$
2160   DECLARE
2161     profile ALIAS FOR $1;
2162   BEGIN
2163     RETURN CASE WHEN 'Staff' IN (select (permission.grp_ancestors(profile)).name) THEN TRUE ELSE FALSE END;
2164   END;
2165 $$ LANGUAGE PLPGSQL STRICT STABLE;
2166
2167
2168 CREATE OR REPLACE FUNCTION migration_tools.is_blank (TEXT) RETURNS BOOLEAN AS $$
2169   BEGIN
2170     RETURN CASE WHEN $1 = '' THEN TRUE ELSE FALSE END;
2171   END;
2172 $$ LANGUAGE PLPGSQL STRICT STABLE;
2173
2174
2175 CREATE OR REPLACE FUNCTION migration_tools.insert_tags (TEXT, TEXT) RETURNS TEXT AS $$
2176
2177   my ($marcxml, $tags) = @_;
2178
2179   use MARC::Record;
2180   use MARC::File::XML;
2181
2182   my $xml = $marcxml;
2183
2184   eval {
2185     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
2186     my $to_insert = MARC::Record->new_from_xml("<record>$tags</record>", 'UTF-8');
2187
2188     my @incumbents = ();
2189
2190     foreach my $field ( $marc->fields() ) {
2191       push @incumbents, $field->as_formatted();
2192     }
2193
2194     foreach $field ( $to_insert->fields() ) {
2195       if (!grep {$_ eq $field->as_formatted()} @incumbents) {
2196         $marc->insert_fields_ordered( ($field) );
2197       }
2198     }
2199
2200     $xml = $marc->as_xml_record;
2201     $xml =~ s/^<\?.+?\?>$//mo;
2202     $xml =~ s/\n//sgo;
2203     $xml =~ s/>\s+</></sgo;
2204   };
2205
2206   return $xml;
2207
2208 $$ LANGUAGE PLPERLU STABLE;
2209
2210 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix( tablename TEXT ) RETURNS VOID AS $$
2211
2212 -- Usage:
2213 --
2214 --   First make sure the circ matrix is loaded and the circulations
2215 --   have been staged to the extent possible (but at the very least
2216 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
2217 --   circ modifiers must also be in place.
2218 --
2219 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
2220 --
2221
2222 DECLARE
2223   circ_lib             INT;
2224   target_copy          INT;
2225   usr                  INT;
2226   is_renewal           BOOLEAN;
2227   this_duration_rule   INT;
2228   this_fine_rule       INT;
2229   this_max_fine_rule   INT;
2230   rcd                  config.rule_circ_duration%ROWTYPE;
2231   rrf                  config.rule_recurring_fine%ROWTYPE;
2232   rmf                  config.rule_max_fine%ROWTYPE;
2233   circ                 INT;
2234   n                    INT := 0;
2235   n_circs              INT;
2236   
2237 BEGIN
2238
2239   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
2240
2241   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
2242
2243     -- Fetch the correct rules for this circulation
2244     EXECUTE ('
2245       SELECT
2246         circ_lib,
2247         target_copy,
2248         usr,
2249         CASE
2250           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
2251           ELSE FALSE
2252         END
2253       FROM ' || tablename || ' WHERE id = ' || circ || ';')
2254       INTO circ_lib, target_copy, usr, is_renewal ;
2255     SELECT
2256       INTO this_duration_rule,
2257            this_fine_rule,
2258            this_max_fine_rule
2259       duration_rule,
2260       recurring_fine_rule,
2261       max_fine_rule
2262       FROM action.item_user_circ_test(
2263         circ_lib,
2264         target_copy,
2265         usr,
2266         is_renewal
2267         );
2268     SELECT INTO rcd * FROM config.rule_circ_duration
2269       WHERE id = this_duration_rule;
2270     SELECT INTO rrf * FROM config.rule_recurring_fine
2271       WHERE id = this_fine_rule;
2272     SELECT INTO rmf * FROM config.rule_max_fine
2273       WHERE id = this_max_fine_rule;
2274
2275     -- Apply the rules to this circulation
2276     EXECUTE ('UPDATE ' || tablename || ' c
2277     SET
2278       duration_rule = rcd.name,
2279       recurring_fine_rule = rrf.name,
2280       max_fine_rule = rmf.name,
2281       duration = rcd.normal,
2282       recurring_fine = rrf.normal,
2283       max_fine =
2284         CASE rmf.is_percent
2285           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
2286           ELSE rmf.amount
2287         END,
2288       renewal_remaining = rcd.max_renewals
2289     FROM
2290       config.rule_circ_duration rcd,
2291       config.rule_recurring_fine rrf,
2292       config.rule_max_fine rmf,
2293                         asset.copy ac
2294     WHERE
2295       rcd.id = ' || this_duration_rule || ' AND
2296       rrf.id = ' || this_fine_rule || ' AND
2297       rmf.id = ' || this_max_fine_rule || ' AND
2298                         ac.id = c.target_copy AND
2299       c.id = ' || circ || ';');
2300
2301     -- Keep track of where we are in the process
2302     n := n + 1;
2303     IF (n % 100 = 0) THEN
2304       RAISE INFO '%', n || ' of ' || n_circs
2305         || ' (' || (100*n/n_circs) || '%) circs updated.';
2306     END IF;
2307
2308   END LOOP;
2309
2310   RETURN;
2311 END;
2312
2313 $$ LANGUAGE plpgsql;
2314
2315 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix_before_20( tablename TEXT ) RETURNS VOID AS $$
2316
2317 -- Usage:
2318 --
2319 --   First make sure the circ matrix is loaded and the circulations
2320 --   have been staged to the extent possible (but at the very least
2321 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
2322 --   circ modifiers must also be in place.
2323 --
2324 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
2325 --
2326
2327 DECLARE
2328   circ_lib             INT;
2329   target_copy          INT;
2330   usr                  INT;
2331   is_renewal           BOOLEAN;
2332   this_duration_rule   INT;
2333   this_fine_rule       INT;
2334   this_max_fine_rule   INT;
2335   rcd                  config.rule_circ_duration%ROWTYPE;
2336   rrf                  config.rule_recurring_fine%ROWTYPE;
2337   rmf                  config.rule_max_fine%ROWTYPE;
2338   circ                 INT;
2339   n                    INT := 0;
2340   n_circs              INT;
2341   
2342 BEGIN
2343
2344   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
2345
2346   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
2347
2348     -- Fetch the correct rules for this circulation
2349     EXECUTE ('
2350       SELECT
2351         circ_lib,
2352         target_copy,
2353         usr,
2354         CASE
2355           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
2356           ELSE FALSE
2357         END
2358       FROM ' || tablename || ' WHERE id = ' || circ || ';')
2359       INTO circ_lib, target_copy, usr, is_renewal ;
2360     SELECT
2361       INTO this_duration_rule,
2362            this_fine_rule,
2363            this_max_fine_rule
2364       duration_rule,
2365       recuring_fine_rule,
2366       max_fine_rule
2367       FROM action.find_circ_matrix_matchpoint(
2368         circ_lib,
2369         target_copy,
2370         usr,
2371         is_renewal
2372         );
2373     SELECT INTO rcd * FROM config.rule_circ_duration
2374       WHERE id = this_duration_rule;
2375     SELECT INTO rrf * FROM config.rule_recurring_fine
2376       WHERE id = this_fine_rule;
2377     SELECT INTO rmf * FROM config.rule_max_fine
2378       WHERE id = this_max_fine_rule;
2379
2380     -- Apply the rules to this circulation
2381     EXECUTE ('UPDATE ' || tablename || ' c
2382     SET
2383       duration_rule = rcd.name,
2384       recuring_fine_rule = rrf.name,
2385       max_fine_rule = rmf.name,
2386       duration = rcd.normal,
2387       recuring_fine = rrf.normal,
2388       max_fine =
2389         CASE rmf.is_percent
2390           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
2391           ELSE rmf.amount
2392         END,
2393       renewal_remaining = rcd.max_renewals
2394     FROM
2395       config.rule_circ_duration rcd,
2396       config.rule_recuring_fine rrf,
2397       config.rule_max_fine rmf,
2398                         asset.copy ac
2399     WHERE
2400       rcd.id = ' || this_duration_rule || ' AND
2401       rrf.id = ' || this_fine_rule || ' AND
2402       rmf.id = ' || this_max_fine_rule || ' AND
2403                         ac.id = c.target_copy AND
2404       c.id = ' || circ || ';');
2405
2406     -- Keep track of where we are in the process
2407     n := n + 1;
2408     IF (n % 100 = 0) THEN
2409       RAISE INFO '%', n || ' of ' || n_circs
2410         || ' (' || (100*n/n_circs) || '%) circs updated.';
2411     END IF;
2412
2413   END LOOP;
2414
2415   RETURN;
2416 END;
2417
2418 $$ LANGUAGE plpgsql;
2419
2420 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix_after_20( tablename TEXT ) RETURNS VOID AS $$
2421
2422 -- Usage:
2423 --
2424 --   First make sure the circ matrix is loaded and the circulations
2425 --   have been staged to the extent possible (but at the very least
2426 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
2427 --   circ modifiers must also be in place.
2428 --
2429 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
2430 --
2431
2432 DECLARE
2433   circ_lib             INT;
2434   target_copy          INT;
2435   usr                  INT;
2436   is_renewal           BOOLEAN;
2437   this_duration_rule   INT;
2438   this_fine_rule       INT;
2439   this_max_fine_rule   INT;
2440   rcd                  config.rule_circ_duration%ROWTYPE;
2441   rrf                  config.rule_recurring_fine%ROWTYPE;
2442   rmf                  config.rule_max_fine%ROWTYPE;
2443   circ                 INT;
2444   n                    INT := 0;
2445   n_circs              INT;
2446   
2447 BEGIN
2448
2449   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
2450
2451   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
2452
2453     -- Fetch the correct rules for this circulation
2454     EXECUTE ('
2455       SELECT
2456         circ_lib,
2457         target_copy,
2458         usr,
2459         CASE
2460           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
2461           ELSE FALSE
2462         END
2463       FROM ' || tablename || ' WHERE id = ' || circ || ';')
2464       INTO circ_lib, target_copy, usr, is_renewal ;
2465     SELECT
2466       INTO this_duration_rule,
2467            this_fine_rule,
2468            this_max_fine_rule
2469       (matchpoint).duration_rule,
2470       (matchpoint).recurring_fine_rule,
2471       (matchpoint).max_fine_rule
2472       FROM action.find_circ_matrix_matchpoint(
2473         circ_lib,
2474         target_copy,
2475         usr,
2476         is_renewal
2477         );
2478     SELECT INTO rcd * FROM config.rule_circ_duration
2479       WHERE id = this_duration_rule;
2480     SELECT INTO rrf * FROM config.rule_recurring_fine
2481       WHERE id = this_fine_rule;
2482     SELECT INTO rmf * FROM config.rule_max_fine
2483       WHERE id = this_max_fine_rule;
2484
2485     -- Apply the rules to this circulation
2486     EXECUTE ('UPDATE ' || tablename || ' c
2487     SET
2488       duration_rule = rcd.name,
2489       recurring_fine_rule = rrf.name,
2490       max_fine_rule = rmf.name,
2491       duration = rcd.normal,
2492       recurring_fine = rrf.normal,
2493       max_fine =
2494         CASE rmf.is_percent
2495           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
2496           ELSE rmf.amount
2497         END,
2498       renewal_remaining = rcd.max_renewals,
2499       grace_period = rrf.grace_period
2500     FROM
2501       config.rule_circ_duration rcd,
2502       config.rule_recurring_fine rrf,
2503       config.rule_max_fine rmf,
2504                         asset.copy ac
2505     WHERE
2506       rcd.id = ' || this_duration_rule || ' AND
2507       rrf.id = ' || this_fine_rule || ' AND
2508       rmf.id = ' || this_max_fine_rule || ' AND
2509                         ac.id = c.target_copy AND
2510       c.id = ' || circ || ';');
2511
2512     -- Keep track of where we are in the process
2513     n := n + 1;
2514     IF (n % 100 = 0) THEN
2515       RAISE INFO '%', n || ' of ' || n_circs
2516         || ' (' || (100*n/n_circs) || '%) circs updated.';
2517     END IF;
2518
2519   END LOOP;
2520
2521   RETURN;
2522 END;
2523
2524 $$ LANGUAGE plpgsql;
2525
2526 -- TODO: make another version of the procedure below that can work with specified copy staging tables
2527 -- The following should track the logic of OpenILS::Application::AppUtils::get_copy_price
2528 CREATE OR REPLACE FUNCTION migration_tools.get_copy_price( item BIGINT ) RETURNS NUMERIC AS $$
2529 DECLARE
2530     context_lib             INT;
2531     charge_lost_on_zero     BOOLEAN;
2532     min_price               NUMERIC;
2533     max_price               NUMERIC;
2534     default_price           NUMERIC;
2535     working_price           NUMERIC;
2536
2537 BEGIN
2538
2539     SELECT INTO context_lib CASE WHEN call_number = -1 THEN circ_lib ELSE owning_lib END
2540         FROM asset.copy ac, asset.call_number acn WHERE ac.call_number = acn.id AND ac.id = item;
2541
2542     SELECT INTO charge_lost_on_zero value
2543         FROM actor.org_unit_ancestor_setting('circ.charge_lost_on_zero',context_lib);
2544
2545     SELECT INTO min_price value
2546         FROM actor.org_unit_ancestor_setting('circ.min_item_price',context_lib);
2547
2548     SELECT INTO max_price value
2549         FROM actor.org_unit_ancestor_setting('circ.max_item_price',context_lib);
2550
2551     SELECT INTO default_price value
2552         FROM actor.org_unit_ancestor_setting('cat.default_item_price',context_lib);
2553
2554     SELECT INTO working_price price FROM asset.copy WHERE id = item;
2555
2556     IF (working_price IS NULL OR (working_price = 0 AND charge_lost_on_zero)) THEN
2557         working_price := default_price;
2558     END IF;
2559
2560     IF (max_price IS NOT NULL AND working_price > max_price) THEN
2561         working_price := max_price;
2562     END IF;
2563
2564     IF (min_price IS NOT NULL AND working_price < min_price) THEN
2565         IF (working_price <> 0 OR charge_lost_on_zero IS NULL OR charge_lost_on_zero) THEN
2566             working_price := min_price;
2567         END IF;
2568     END IF;
2569
2570     RETURN working_price;
2571
2572 END;
2573
2574 $$ LANGUAGE plpgsql;
2575
2576 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix_to_specific_circ( tablename TEXT, circ BIGINT ) RETURNS VOID AS $$
2577
2578 -- Usage:
2579 --
2580 --   First make sure the circ matrix is loaded and the circulations
2581 --   have been staged to the extent possible (but at the very least
2582 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
2583 --   circ modifiers must also be in place.
2584 --
2585 --   SELECT migration_tools.apply_circ_matrix_to_specific_circ('m_nwrl.action_circulation', 18391960);
2586 --
2587
2588 DECLARE
2589   circ_lib             INT;
2590   target_copy          INT;
2591   usr                  INT;
2592   is_renewal           BOOLEAN;
2593   this_duration_rule   INT;
2594   this_fine_rule       INT;
2595   this_max_fine_rule   INT;
2596   rcd                  config.rule_circ_duration%ROWTYPE;
2597   rrf                  config.rule_recurring_fine%ROWTYPE;
2598   rmf                  config.rule_max_fine%ROWTYPE;
2599   n                    INT := 0;
2600   n_circs              INT := 1;
2601   
2602 BEGIN
2603
2604   --EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
2605
2606   --FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
2607
2608     -- Fetch the correct rules for this circulation
2609     EXECUTE ('
2610       SELECT
2611         circ_lib,
2612         target_copy,
2613         usr,
2614         CASE
2615           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
2616           ELSE FALSE
2617         END
2618       FROM ' || tablename || ' WHERE id = ' || circ || ';')
2619       INTO circ_lib, target_copy, usr, is_renewal ;
2620     SELECT
2621       INTO this_duration_rule,
2622            this_fine_rule,
2623            this_max_fine_rule
2624       (matchpoint).duration_rule,
2625       (matchpoint).recurring_fine_rule,
2626       (matchpoint).max_fine_rule
2627       FROM action.find_circ_matrix_matchpoint(
2628         circ_lib,
2629         target_copy,
2630         usr,
2631         is_renewal
2632         );
2633     SELECT INTO rcd * FROM config.rule_circ_duration
2634       WHERE id = this_duration_rule;
2635     SELECT INTO rrf * FROM config.rule_recurring_fine
2636       WHERE id = this_fine_rule;
2637     SELECT INTO rmf * FROM config.rule_max_fine
2638       WHERE id = this_max_fine_rule;
2639
2640     -- Apply the rules to this circulation
2641     EXECUTE ('UPDATE ' || tablename || ' c
2642     SET
2643       duration_rule = rcd.name,
2644       recurring_fine_rule = rrf.name,
2645       max_fine_rule = rmf.name,
2646       duration = rcd.normal,
2647       recurring_fine = rrf.normal,
2648       max_fine =
2649         CASE rmf.is_percent
2650           WHEN TRUE THEN (rmf.amount / 100.0) * migration_tools.get_copy_price(ac.id)
2651           ELSE rmf.amount
2652         END,
2653       renewal_remaining = rcd.max_renewals,
2654       grace_period = rrf.grace_period
2655     FROM
2656       config.rule_circ_duration rcd,
2657       config.rule_recurring_fine rrf,
2658       config.rule_max_fine rmf,
2659                         asset.copy ac
2660     WHERE
2661       rcd.id = ' || this_duration_rule || ' AND
2662       rrf.id = ' || this_fine_rule || ' AND
2663       rmf.id = ' || this_max_fine_rule || ' AND
2664                         ac.id = c.target_copy AND
2665       c.id = ' || circ || ';');
2666
2667     -- Keep track of where we are in the process
2668     n := n + 1;
2669     IF (n % 100 = 0) THEN
2670       RAISE INFO '%', n || ' of ' || n_circs
2671         || ' (' || (100*n/n_circs) || '%) circs updated.';
2672     END IF;
2673
2674   --END LOOP;
2675
2676   RETURN;
2677 END;
2678
2679 $$ LANGUAGE plpgsql;
2680
2681
2682
2683
2684 CREATE OR REPLACE FUNCTION migration_tools.stage_not_applicable_asset_stat_cats( schemaname TEXT ) RETURNS VOID AS $$
2685
2686 -- USAGE: Make sure the stat_cat and stat_cat_entry tables are populated, including exactly one 'Not Applicable' entry per stat cat.
2687 --        Then SELECT migration_tools.stage_not_applicable_asset_stat_cats('m_foo');
2688
2689 -- TODO: Make a variant that will go directly to production tables -- which would be useful for retrofixing the absence of N/A cats.
2690 -- TODO: Add a similar tool for actor stat cats, which behave differently.
2691
2692 DECLARE
2693         c                    TEXT := schemaname || '.asset_copy_legacy';
2694         sc                                                                       TEXT := schemaname || '.asset_stat_cat';
2695         sce                                                                      TEXT := schemaname || '.asset_stat_cat_entry';
2696         scecm                                                            TEXT := schemaname || '.asset_stat_cat_entry_copy_map';
2697         stat_cat                                                 INT;
2698   stat_cat_entry       INT;
2699   
2700 BEGIN
2701
2702   FOR stat_cat IN EXECUTE ('SELECT id FROM ' || sc) LOOP
2703
2704                 EXECUTE ('SELECT id FROM ' || sce || ' WHERE stat_cat = ' || stat_cat || E' AND value = \'Not Applicable\';') INTO stat_cat_entry;
2705
2706                 EXECUTE ('INSERT INTO ' || scecm || ' (owning_copy, stat_cat, stat_cat_entry)
2707                                                         SELECT c.id, ' || stat_cat || ', ' || stat_cat_entry || ' FROM ' || c || ' c WHERE c.id NOT IN
2708                                                         (SELECT owning_copy FROM ' || scecm || ' WHERE stat_cat = ' || stat_cat || ');');
2709
2710   END LOOP;
2711
2712   RETURN;
2713 END;
2714
2715 $$ LANGUAGE plpgsql;
2716
2717 CREATE OR REPLACE FUNCTION migration_tools.assign_standing_penalties ( ) RETURNS VOID AS $$
2718
2719 -- USAGE: Once circulation data has been loaded, and group penalty thresholds have been set up, run this.
2720 --        This will assign standing penalties as needed.
2721
2722 DECLARE
2723   org_unit  INT;
2724   usr       INT;
2725
2726 BEGIN
2727
2728   FOR org_unit IN EXECUTE ('SELECT DISTINCT org_unit FROM permission.grp_penalty_threshold;') LOOP
2729
2730     FOR usr IN EXECUTE ('SELECT id FROM actor.usr WHERE NOT deleted;') LOOP
2731   
2732       EXECUTE('SELECT actor.calculate_system_penalties(' || usr || ', ' || org_unit || ');');
2733
2734     END LOOP;
2735
2736   END LOOP;
2737
2738   RETURN;
2739
2740 END;
2741
2742 $$ LANGUAGE plpgsql;
2743
2744
2745 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_pristine_database () RETURNS VOID AS $$
2746
2747 BEGIN
2748   INSERT INTO metabib.metarecord (fingerprint, master_record)
2749     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
2750       FROM  biblio.record_entry b
2751       WHERE NOT b.deleted
2752         AND b.id IN (SELECT r.id FROM biblio.record_entry r LEFT JOIN metabib.metarecord_source_map k ON (k.source = r.id) WHERE k.id IS NULL AND r.fingerprint IS NOT NULL)
2753         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
2754       ORDER BY b.fingerprint, b.quality DESC;
2755   INSERT INTO metabib.metarecord_source_map (metarecord, source)
2756     SELECT  m.id, r.id
2757       FROM  biblio.record_entry r
2758       JOIN  metabib.metarecord m USING (fingerprint)
2759      WHERE  NOT r.deleted;
2760 END;
2761   
2762 $$ LANGUAGE plpgsql;
2763
2764
2765 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_incumbent_database () RETURNS VOID AS $$
2766
2767 BEGIN
2768   INSERT INTO metabib.metarecord (fingerprint, master_record)
2769     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
2770       FROM  biblio.record_entry b
2771       WHERE NOT b.deleted
2772         AND b.id IN (SELECT r.id FROM biblio.record_entry r LEFT JOIN metabib.metarecord_source_map k ON (k.source = r.id) WHERE k.id IS NULL AND r.fingerprint IS NOT NULL)
2773         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
2774       ORDER BY b.fingerprint, b.quality DESC;
2775   INSERT INTO metabib.metarecord_source_map (metarecord, source)
2776     SELECT  m.id, r.id
2777       FROM  biblio.record_entry r
2778         JOIN metabib.metarecord m USING (fingerprint)
2779       WHERE NOT r.deleted
2780         AND r.id IN (SELECT b.id FROM biblio.record_entry b LEFT JOIN metabib.metarecord_source_map k ON (k.source = b.id) WHERE k.id IS NULL);
2781 END;
2782     
2783 $$ LANGUAGE plpgsql;
2784
2785
2786 CREATE OR REPLACE FUNCTION migration_tools.create_cards( schemaname TEXT ) RETURNS VOID AS $$
2787
2788 -- USAGE: Make sure the patrons are staged in schemaname.actor_usr_legacy and have 'usrname' assigned.
2789 --        Then SELECT migration_tools.create_cards('m_foo');
2790
2791 DECLARE
2792         u                    TEXT := schemaname || '.actor_usr_legacy';
2793         c                    TEXT := schemaname || '.actor_card';
2794   
2795 BEGIN
2796
2797         EXECUTE ('DELETE FROM ' || c || ';');
2798         EXECUTE ('INSERT INTO ' || c || ' (usr, barcode) SELECT id, usrname FROM ' || u || ';');
2799         EXECUTE ('UPDATE ' || u || ' u SET card = c.id FROM ' || c || ' c WHERE c.usr = u.id;');
2800
2801   RETURN;
2802
2803 END;
2804
2805 $$ LANGUAGE plpgsql;
2806
2807
2808 CREATE OR REPLACE FUNCTION migration_tools.insert_856_9_conditional (TEXT, TEXT) RETURNS TEXT AS $$
2809
2810   ## USAGE: UPDATE biblio.record_entry SET marc = migration_tools.insert_856_9(marc, 'ABC') WHERE [...];
2811
2812   my ($marcxml, $shortname) = @_;
2813
2814   use MARC::Record;
2815   use MARC::File::XML;
2816
2817   my $xml = $marcxml;
2818
2819   eval {
2820     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
2821
2822     foreach my $field ( $marc->field('856') ) {
2823       if ( scalar(grep( /(contentreserve|netlibrary|overdrive)\.com/i, $field->subfield('u'))) > 0 &&
2824            ! ( $field->as_string('9') =~ m/$shortname/ ) ) {
2825         $field->add_subfields( '9' => $shortname );
2826                                 $field->update( ind2 => '0');
2827       }
2828     }
2829
2830     $xml = $marc->as_xml_record;
2831     $xml =~ s/^<\?.+?\?>$//mo;
2832     $xml =~ s/\n//sgo;
2833     $xml =~ s/>\s+</></sgo;
2834   };
2835
2836   return $xml;
2837
2838 $$ LANGUAGE PLPERLU STABLE;
2839
2840 CREATE OR REPLACE FUNCTION migration_tools.insert_856_9 (TEXT, TEXT) RETURNS TEXT AS $$
2841
2842   ## USAGE: UPDATE biblio.record_entry SET marc = migration_tools.insert_856_9(marc, 'ABC') WHERE [...];
2843
2844   my ($marcxml, $shortname) = @_;
2845
2846   use MARC::Record;
2847   use MARC::File::XML;
2848
2849   my $xml = $marcxml;
2850
2851   eval {
2852     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
2853
2854     foreach my $field ( $marc->field('856') ) {
2855       if ( ! $field->as_string('9') ) {
2856         $field->add_subfields( '9' => $shortname );
2857       }
2858     }
2859
2860     $xml = $marc->as_xml_record;
2861     $xml =~ s/^<\?.+?\?>$//mo;
2862     $xml =~ s/\n//sgo;
2863     $xml =~ s/>\s+</></sgo;
2864   };
2865
2866   return $xml;
2867
2868 $$ LANGUAGE PLPERLU STABLE;
2869
2870
2871 CREATE OR REPLACE FUNCTION migration_tools.change_call_number(copy_id BIGINT, new_label TEXT, cn_class BIGINT) RETURNS VOID AS $$
2872
2873 DECLARE
2874   old_volume   BIGINT;
2875   new_volume   BIGINT;
2876   bib          BIGINT;
2877   owner        INTEGER;
2878   old_label    TEXT;
2879   remainder    BIGINT;
2880
2881 BEGIN
2882
2883   -- Bail out if asked to change the label to ##URI##
2884   IF new_label = '##URI##' THEN
2885     RETURN;
2886   END IF;
2887
2888   -- Gather information
2889   SELECT call_number INTO old_volume FROM asset.copy WHERE id = copy_id;
2890   SELECT record INTO bib FROM asset.call_number WHERE id = old_volume;
2891   SELECT owning_lib, label INTO owner, old_label FROM asset.call_number WHERE id = old_volume;
2892
2893   -- Bail out if the label already is ##URI##
2894   IF old_label = '##URI##' THEN
2895     RETURN;
2896   END IF;
2897
2898   -- Bail out if the call number label is already correct
2899   IF new_volume = old_volume THEN
2900     RETURN;
2901   END IF;
2902
2903   -- Check whether we already have a destination volume available
2904   SELECT id INTO new_volume FROM asset.call_number 
2905     WHERE 
2906       record = bib AND
2907       owning_lib = owner AND
2908       label = new_label AND
2909       NOT deleted;
2910
2911   -- Create destination volume if needed
2912   IF NOT FOUND THEN
2913     INSERT INTO asset.call_number (creator, editor, record, owning_lib, label, label_class) 
2914       VALUES (1, 1, bib, owner, new_label, cn_class);
2915     SELECT id INTO new_volume FROM asset.call_number
2916       WHERE 
2917         record = bib AND
2918         owning_lib = owner AND
2919         label = new_label AND
2920         NOT deleted;
2921   END IF;
2922
2923   -- Move copy to destination
2924   UPDATE asset.copy SET call_number = new_volume WHERE id = copy_id;
2925
2926   -- Delete source volume if it is now empty
2927   SELECT id INTO remainder FROM asset.copy WHERE call_number = old_volume AND NOT deleted;
2928   IF NOT FOUND THEN
2929     DELETE FROM asset.call_number WHERE id = old_volume;
2930   END IF;
2931
2932 END;
2933
2934 $$ LANGUAGE plpgsql;
2935
2936 CREATE OR REPLACE FUNCTION migration_tools.zip_to_city_state_county (TEXT) RETURNS TEXT[] AS $$
2937
2938         my $input = $_[0];
2939         my %zipdata;
2940
2941         open (FH, '<', '/openils/var/data/zips.txt') or return ('No File Found', 'No File Found', 'No File Found');
2942
2943         while (<FH>) {
2944                 chomp;
2945                 my ($junk, $state, $city, $zip, $foo, $bar, $county, $baz, $morejunk) = split(/\|/);
2946                 $zipdata{$zip} = [$city, $state, $county];
2947         }
2948
2949         if (defined $zipdata{$input}) {
2950                 my ($city, $state, $county) = @{$zipdata{$input}};
2951                 return [$city, $state, $county];
2952         } elsif (defined $zipdata{substr $input, 0, 5}) {
2953                 my ($city, $state, $county) = @{$zipdata{substr $input, 0, 5}};
2954                 return [$city, $state, $county];
2955         } else {
2956                 return ['ZIP not found', 'ZIP not found', 'ZIP not found'];
2957         }
2958   
2959 $$ LANGUAGE PLPERLU STABLE;
2960
2961 CREATE OR REPLACE FUNCTION migration_tools.check_ou_depth ( ) RETURNS VOID AS $$
2962
2963 DECLARE
2964   ou  INT;
2965         org_unit_depth INT;
2966         ou_parent INT;
2967         parent_depth INT;
2968   errors_found BOOLEAN;
2969         ou_shortname TEXT;
2970         parent_shortname TEXT;
2971         ou_type_name TEXT;
2972         parent_type TEXT;
2973         type_id INT;
2974         type_depth INT;
2975         type_parent INT;
2976         type_parent_depth INT;
2977         proper_parent TEXT;
2978
2979 BEGIN
2980
2981         errors_found := FALSE;
2982
2983 -- Checking actor.org_unit_type
2984
2985         FOR type_id IN EXECUTE ('SELECT id FROM actor.org_unit_type ORDER BY id;') LOOP
2986
2987                 SELECT depth FROM actor.org_unit_type WHERE id = type_id INTO type_depth;
2988                 SELECT parent FROM actor.org_unit_type WHERE id = type_id INTO type_parent;
2989
2990                 IF type_parent IS NOT NULL THEN
2991
2992                         SELECT depth FROM actor.org_unit_type WHERE id = type_parent INTO type_parent_depth;
2993
2994                         IF type_depth - type_parent_depth <> 1 THEN
2995                                 SELECT name FROM actor.org_unit_type WHERE id = type_id INTO ou_type_name;
2996                                 SELECT name FROM actor.org_unit_type WHERE id = type_parent INTO parent_type;
2997                                 RAISE INFO 'The % org unit type has a depth of %, but its parent org unit type, %, has a depth of %.',
2998                                         ou_type_name, type_depth, parent_type, type_parent_depth;
2999                                 errors_found := TRUE;
3000
3001                         END IF;
3002
3003                 END IF;
3004
3005         END LOOP;
3006
3007 -- Checking actor.org_unit
3008
3009   FOR ou IN EXECUTE ('SELECT id FROM actor.org_unit ORDER BY shortname;') LOOP
3010
3011                 SELECT parent_ou FROM actor.org_unit WHERE id = ou INTO ou_parent;
3012                 SELECT t.depth FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou INTO org_unit_depth;
3013                 SELECT t.depth FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou_parent INTO parent_depth;
3014                 SELECT shortname FROM actor.org_unit WHERE id = ou INTO ou_shortname;
3015                 SELECT shortname FROM actor.org_unit WHERE id = ou_parent INTO parent_shortname;
3016                 SELECT t.name FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou INTO ou_type_name;
3017                 SELECT t.name FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou_parent INTO parent_type;
3018
3019                 IF ou_parent IS NOT NULL THEN
3020
3021                         IF      (org_unit_depth - parent_depth <> 1) OR (
3022                                 (SELECT parent FROM actor.org_unit_type WHERE name = ou_type_name) <> (SELECT id FROM actor.org_unit_type WHERE name = parent_type)
3023                         ) THEN
3024                                 RAISE INFO '% (org unit %) is a % (depth %) but its parent, % (org unit %), is a % (depth %).', 
3025                                         ou_shortname, ou, ou_type_name, org_unit_depth, parent_shortname, ou_parent, parent_type, parent_depth;
3026                                 errors_found := TRUE;
3027                         END IF;
3028
3029                 END IF;
3030
3031   END LOOP;
3032
3033         IF NOT errors_found THEN
3034                 RAISE INFO 'No errors found.';
3035         END IF;
3036
3037   RETURN;
3038
3039 END;
3040
3041 $$ LANGUAGE plpgsql;
3042
3043
3044 CREATE OR REPLACE FUNCTION migration_tools.refresh_opac_visible_copies ( ) RETURNS VOID AS $$
3045
3046 BEGIN   
3047
3048         DELETE FROM asset.opac_visible_copies;
3049
3050         INSERT INTO asset.opac_visible_copies (id, circ_lib, record)
3051                 SELECT DISTINCT
3052                         cp.id, cp.circ_lib, cn.record
3053                 FROM
3054                         asset.copy cp
3055                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
3056                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3057                         JOIN asset.copy_location cl ON (cp.location = cl.id)
3058                         JOIN config.copy_status cs ON (cp.status = cs.id)
3059                         JOIN biblio.record_entry b ON (cn.record = b.id)
3060                 WHERE 
3061                         NOT cp.deleted AND
3062                         NOT cn.deleted AND
3063                         NOT b.deleted AND
3064                         cs.opac_visible AND
3065                         cl.opac_visible AND
3066                         cp.opac_visible AND
3067                         a.opac_visible AND
3068                         cp.id NOT IN (SELECT id FROM asset.opac_visible_copies);
3069
3070 END;
3071
3072 $$ LANGUAGE plpgsql;
3073
3074
3075 CREATE OR REPLACE FUNCTION migration_tools.change_owning_lib(copy_id BIGINT, new_owning_lib INTEGER) RETURNS VOID AS $$
3076
3077 DECLARE
3078   old_volume     BIGINT;
3079   new_volume     BIGINT;
3080   bib            BIGINT;
3081   old_owning_lib INTEGER;
3082         old_label      TEXT;
3083   remainder      BIGINT;
3084
3085 BEGIN
3086
3087   -- Gather information
3088   SELECT call_number INTO old_volume FROM asset.copy WHERE id = copy_id;
3089   SELECT record INTO bib FROM asset.call_number WHERE id = old_volume;
3090   SELECT owning_lib, label INTO old_owning_lib, old_label FROM asset.call_number WHERE id = old_volume;
3091
3092         -- Bail out if the new_owning_lib is not the ID of an org_unit
3093         IF new_owning_lib NOT IN (SELECT id FROM actor.org_unit) THEN
3094                 RAISE WARNING 
3095                         '% is not a valid actor.org_unit ID; no change made.', 
3096                                 new_owning_lib;
3097                 RETURN;
3098         END IF;
3099
3100   -- Bail out discreetly if the owning_lib is already correct
3101   IF new_owning_lib = old_owning_lib THEN
3102     RETURN;
3103   END IF;
3104
3105   -- Check whether we already have a destination volume available
3106   SELECT id INTO new_volume FROM asset.call_number 
3107     WHERE 
3108       record = bib AND
3109       owning_lib = new_owning_lib AND
3110       label = old_label AND
3111       NOT deleted;
3112
3113   -- Create destination volume if needed
3114   IF NOT FOUND THEN
3115     INSERT INTO asset.call_number (creator, editor, record, owning_lib, label) 
3116       VALUES (1, 1, bib, new_owning_lib, old_label);
3117     SELECT id INTO new_volume FROM asset.call_number
3118       WHERE 
3119         record = bib AND
3120         owning_lib = new_owning_lib AND
3121         label = old_label AND
3122         NOT deleted;
3123   END IF;
3124
3125   -- Move copy to destination
3126   UPDATE asset.copy SET call_number = new_volume WHERE id = copy_id;
3127
3128   -- Delete source volume if it is now empty
3129   SELECT id INTO remainder FROM asset.copy WHERE call_number = old_volume AND NOT deleted;
3130   IF NOT FOUND THEN
3131     DELETE FROM asset.call_number WHERE id = old_volume;
3132   END IF;
3133
3134 END;
3135
3136 $$ LANGUAGE plpgsql;
3137
3138
3139 CREATE OR REPLACE FUNCTION migration_tools.change_owning_lib(copy_id BIGINT, new_owner TEXT) RETURNS VOID AS $$
3140
3141 -- You can use shortnames with this function, which looks up the org unit ID and passes it to change_owning_lib(BIGINT,INTEGER).
3142
3143 DECLARE
3144         new_owning_lib  INTEGER;
3145
3146 BEGIN
3147
3148         -- Parse the new_owner as an org unit ID or shortname
3149         IF new_owner IN (SELECT shortname FROM actor.org_unit) THEN
3150                 SELECT id INTO new_owning_lib FROM actor.org_unit WHERE shortname = new_owner;
3151                 PERFORM migration_tools.change_owning_lib(copy_id, new_owning_lib);
3152         ELSIF new_owner ~ E'^[0-9]+$' THEN
3153                 IF new_owner::INTEGER IN (SELECT id FROM actor.org_unit) THEN
3154                         RAISE INFO 
3155                                 '%',
3156                                 E'You don\'t need to put the actor.org_unit ID in quotes; '
3157                                         || E'if you put it in quotes, I\'m going to try to parse it as a shortname first.';
3158                         new_owning_lib := new_owner::INTEGER;
3159                 PERFORM migration_tools.change_owning_lib(copy_id, new_owning_lib);
3160                 END IF;
3161         ELSE
3162                 RAISE WARNING 
3163                         '% is not a valid actor.org_unit shortname or ID; no change made.', 
3164                         new_owning_lib;
3165                 RETURN;
3166         END IF;
3167
3168 END;
3169
3170 $$ LANGUAGE plpgsql;
3171
3172 CREATE OR REPLACE FUNCTION migration_tools.marc_parses( TEXT ) RETURNS BOOLEAN AS $func$
3173
3174 use MARC::Record;
3175 use MARC::File::XML (BinaryEncoding => 'UTF-8');
3176 use MARC::Charset;
3177
3178 MARC::Charset->assume_unicode(1);
3179
3180 my $xml = shift;
3181
3182 eval {
3183     my $r = MARC::Record->new_from_xml( $xml );
3184     my $output_xml = $r->as_xml_record();
3185 };
3186 if ($@) {
3187     return 0;
3188 } else {
3189     return 1;
3190 }
3191
3192 $func$ LANGUAGE PLPERLU;
3193 COMMENT ON FUNCTION migration_tools.marc_parses(TEXT) IS 'Return boolean indicating if MARCXML string is parseable by MARC::File::XML';
3194
3195 CREATE OR REPLACE FUNCTION migration_tools.simple_export_library_config(dir TEXT, orgs INT[]) RETURNS VOID AS $FUNC$
3196 BEGIN
3197    EXECUTE $$COPY (SELECT * FROM actor.hours_of_operation WHERE id IN ($$ ||
3198            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
3199            $$) TO '$$ ||  dir || $$/actor_hours_of_operation'$$;
3200    EXECUTE $$COPY (SELECT org_unit, close_start, close_end, reason FROM actor.org_unit_closed WHERE org_unit IN ($$ ||
3201            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
3202            $$) TO '$$ ||  dir || $$/actor_org_unit_closed'$$;
3203    EXECUTE $$COPY (SELECT org_unit, name, value FROM actor.org_unit_setting WHERE org_unit IN ($$ ||
3204            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
3205            $$) TO '$$ ||  dir || $$/actor_org_unit_setting'$$;
3206    EXECUTE $$COPY (SELECT name, owning_lib, holdable, hold_verify, opac_visible, circulate FROM asset.copy_location WHERE owning_lib IN ($$ ||
3207            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
3208            $$) TO '$$ ||  dir || $$/asset_copy_location'$$;
3209    EXECUTE $$COPY (SELECT grp, org_unit, penalty, threshold FROM permission.grp_penalty_threshold WHERE org_unit IN ($$ ||
3210            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
3211            $$) TO '$$ ||  dir || $$/permission_grp_penalty_threshold'$$;
3212    EXECUTE $$COPY (SELECT owning_lib, label, label_sortkey FROM asset.call_number_prefix WHERE owning_lib IN ($$ ||
3213            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
3214            $$) TO '$$ ||  dir || $$/asset_call_number_prefix'$$;
3215    EXECUTE $$COPY (SELECT owning_lib, label, label_sortkey FROM asset.call_number_suffix WHERE owning_lib IN ($$ ||
3216            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
3217            $$) TO '$$ ||  dir || $$/asset_call_number_suffix'$$;
3218    EXECUTE $$COPY config.rule_circ_duration TO '$$ ||  dir || $$/config_rule_circ_duration'$$;
3219    EXECUTE $$COPY config.rule_age_hold_protect TO '$$ ||  dir || $$/config_rule_age_hold_protect'$$;
3220    EXECUTE $$COPY config.rule_max_fine TO '$$ ||  dir || $$/config_rule_max_fine'$$;
3221    EXECUTE $$COPY config.rule_recurring_fine TO '$$ ||  dir || $$/config_rule_recurring_fine'$$;
3222    EXECUTE $$COPY permission.grp_tree TO '$$ ||  dir || $$/permission_grp_tree'$$;
3223 END;
3224 $FUNC$ LANGUAGE PLPGSQL;
3225
3226 CREATE OR REPLACE FUNCTION migration_tools.simple_import_library_config(dir TEXT) RETURNS VOID AS $FUNC$
3227 BEGIN
3228    EXECUTE $$COPY actor.hours_of_operation FROM '$$ ||  dir || $$/actor_hours_of_operation'$$;
3229    EXECUTE $$COPY actor.org_unit_closed (org_unit, close_start, close_end, reason) FROM '$$ ||  dir || $$/actor_org_unit_closed'$$;
3230    EXECUTE $$COPY actor.org_unit_setting (org_unit, name, value) FROM '$$ ||  dir || $$/actor_org_unit_setting'$$;
3231    EXECUTE $$COPY asset.copy_location (name, owning_lib, holdable, hold_verify, opac_visible, circulate) FROM '$$ ||  dir || $$/asset_copy_location'$$;
3232    EXECUTE $$COPY permission.grp_penalty_threshold (grp, org_unit, penalty, threshold) FROM '$$ ||  dir || $$/permission_grp_penalty_threshold'$$;
3233    EXECUTE $$COPY asset.call_number_prefix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/asset_call_number_prefix'$$;
3234    EXECUTE $$COPY asset.call_number_suffix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/asset_call_number_suffix'$$;
3235
3236    -- import any new circ rules
3237    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_circ_duration', 'id', 'name');
3238    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_age_hold_protect', 'id', 'name');
3239    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_max_fine', 'id', 'name');
3240    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_recurring_fine', 'id', 'name');
3241
3242    -- and permission groups
3243    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'permission', 'grp_tree', 'id', 'name');
3244
3245 END;
3246 $FUNC$ LANGUAGE PLPGSQL;
3247
3248
3249 CREATE OR REPLACE FUNCTION migration_tools.simple_import_new_rows_by_value(dir TEXT, schemaname TEXT, tablename TEXT, idcol TEXT, matchcol TEXT) RETURNS VOID AS $FUNC$
3250 DECLARE
3251     name TEXT;
3252     loopq TEXT;
3253     existsq TEXT;
3254     ct INTEGER;
3255     cols TEXT[];
3256     copyst TEXT;
3257 BEGIN
3258     EXECUTE $$DROP TABLE IF EXISTS tmp_$$ || tablename;
3259     EXECUTE $$CREATE TEMPORARY TABLE tmp_$$ || tablename || $$ AS SELECT * FROM $$ || schemaname || '.' || tablename || $$ LIMIT 0$$;
3260     EXECUTE $$COPY tmp_$$ || tablename || $$ FROM '$$ ||  dir || '/' || schemaname || '_' || tablename || $$'$$;
3261     loopq := 'SELECT ' || matchcol || ' FROM tmp_' || tablename || ' ORDER BY ' || idcol;
3262     existsq := 'SELECT COUNT(*) FROM ' || schemaname || '.' || tablename || ' WHERE ' || matchcol || ' = $1';
3263     SELECT ARRAY_AGG(column_name::TEXT) INTO cols FROM information_schema.columns WHERE table_schema = schemaname AND table_name = tablename AND column_name <> idcol;
3264     FOR name IN EXECUTE loopq LOOP
3265        EXECUTE existsq INTO ct USING name;
3266        IF ct = 0 THEN
3267            RAISE NOTICE 'inserting %.% row for %', schemaname, tablename, name;
3268            copyst := 'INSERT INTO ' || schemaname || '.' || tablename || ' (' || ARRAY_TO_STRING(cols, ',') || ') SELECT ' || ARRAY_TO_STRING(cols, ',') || 
3269                      ' FROM tmp_' || tablename || ' WHERE ' || matchcol || ' = $1';
3270            EXECUTE copyst USING name;
3271        END IF;
3272     END LOOP;
3273 END;
3274 $FUNC$ LANGUAGE PLPGSQL;
3275
3276 CREATE OR REPLACE FUNCTION migration_tools.split_rows_on_column_with_delimiter(schemaname TEXT, tablename TEXT, matchcol TEXT, delimiter TEXT) RETURNS VOID AS $FUNC$
3277 DECLARE
3278     id BIGINT;
3279     loopq TEXT;
3280     cols TEXT[];
3281     splitst TEXT;
3282 BEGIN
3283     loopq := 'SELECT id FROM ' || schemaname || '.' || tablename || ' WHERE ' || matchcol || ' ~ $1 ORDER BY id';
3284     SELECT ARRAY_AGG(column_name::TEXT) INTO cols FROM information_schema.columns WHERE table_schema = schemaname AND table_name = tablename AND column_name <> 'id' AND column_name <> matchcol;
3285     FOR id IN EXECUTE loopq USING delimiter LOOP
3286        RAISE NOTICE 'splitting row from %.% with id = %', schemaname, tablename, id;
3287        splitst := 'INSERT INTO ' || schemaname || '.' || tablename || ' (' || ARRAY_TO_STRING(cols, ',') || ', ' || matchcol || ') SELECT ' || ARRAY_TO_STRING(cols, ',') || ', s.token ' ||
3288                  ' FROM ' || schemaname || '.' || tablename || ' t, UNNEST(STRING_TO_ARRAY(t.' || matchcol || ', $2)) s(token) WHERE id = $1';
3289        EXECUTE splitst USING id, delimiter;
3290     END LOOP;
3291 END;
3292 $FUNC$ LANGUAGE PLPGSQL;
3293
3294 CREATE OR REPLACE FUNCTION migration_tools.merge_marc_fields( TEXT, TEXT, TEXT[] ) RETURNS TEXT AS $func$
3295
3296 use strict;
3297 use warnings;
3298
3299 use MARC::Record;
3300 use MARC::File::XML (BinaryEncoding => 'UTF-8');
3301 use MARC::Charset;
3302
3303 MARC::Charset->assume_unicode(1);
3304
3305 my $target_xml = shift;
3306 my $source_xml = shift;
3307 my $tags = shift;
3308
3309 my $target;
3310 my $source;
3311
3312 eval { $target = MARC::Record->new_from_xml( $target_xml ); };
3313 if ($@) {
3314     return;
3315 }
3316 eval { $source = MARC::Record->new_from_xml( $source_xml ); };
3317 if ($@) {
3318     return;
3319 }
3320
3321 my $source_id = $source->subfield('901', 'c');
3322 $source_id = $source->subfield('903', 'a') unless $source_id;
3323 my $target_id = $target->subfield('901', 'c');
3324 $target_id = $target->subfield('903', 'a') unless $target_id;
3325
3326 my %existing_fields;
3327 foreach my $tag (@$tags) {
3328     my %existing_fields = map { $_->as_formatted() => 1 } $target->field($tag);
3329     my @to_add = grep { not exists $existing_fields{$_->as_formatted()} } $source->field($tag);
3330     $target->insert_fields_ordered(map { $_->clone() } @to_add);
3331     if (@to_add) {
3332         elog(NOTICE, "Merged $tag tag(s) from $source_id to $target_id");
3333     }
3334 }
3335
3336 my $xml = $target->as_xml_record;
3337 $xml =~ s/^<\?.+?\?>$//mo;
3338 $xml =~ s/\n//sgo;
3339 $xml =~ s/>\s+</></sgo;
3340
3341 return $xml;
3342
3343 $func$ LANGUAGE PLPERLU;
3344 COMMENT ON FUNCTION migration_tools.merge_marc_fields( TEXT, TEXT, TEXT[] ) IS 'Given two MARCXML strings and an array of tags, returns MARCXML representing the merge of the specified fields from the second MARCXML record into the first.';
3345
3346 CREATE OR REPLACE FUNCTION migration_tools.make_stub_bib (text[], text[]) RETURNS TEXT AS $func$
3347
3348 use strict;
3349 use warnings;
3350
3351 use MARC::Record;
3352 use MARC::File::XML (BinaryEncoding => 'UTF-8');
3353 use Text::CSV;
3354
3355 my $in_tags = shift;
3356 my $in_values = shift;
3357
3358 # hack-and-slash parsing of array-passed-as-string;
3359 # this can go away once everybody is running Postgres 9.1+
3360 my $csv = Text::CSV->new({binary => 1});
3361 $in_tags =~ s/^{//;
3362 $in_tags =~ s/}$//;
3363 my $status = $csv->parse($in_tags);
3364 my $tags = [ $csv->fields() ];
3365 $in_values =~ s/^{//;
3366 $in_values =~ s/}$//;
3367 $status = $csv->parse($in_values);
3368 my $values = [ $csv->fields() ];
3369
3370 my $marc = MARC::Record->new();
3371
3372 $marc->leader('00000nam a22000007  4500');
3373 $marc->append_fields(MARC::Field->new('008', '000000s                       000   eng d'));
3374
3375 foreach my $i (0..$#$tags) {
3376     my ($tag, $sf);
3377     if ($tags->[$i] =~ /^(\d{3})([0-9a-z])$/) {
3378         $tag = $1;
3379         $sf = $2;
3380         $marc->append_fields(MARC::Field->new($tag, ' ', ' ', $sf => $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
3381     } elsif ($tags->[$i] =~ /^(\d{3})$/) {
3382         $tag = $1;
3383         $marc->append_fields(MARC::Field->new($tag, $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
3384     }
3385 }
3386
3387 my $xml = $marc->as_xml_record;
3388 $xml =~ s/^<\?.+?\?>$//mo;
3389 $xml =~ s/\n//sgo;
3390 $xml =~ s/>\s+</></sgo;
3391
3392 return $xml;
3393
3394 $func$ LANGUAGE PLPERLU;
3395 COMMENT ON FUNCTION migration_tools.make_stub_bib (text[], text[]) IS $$Simple function to create a stub MARCXML bib from a set of columns.
3396 The first argument is an array of tag/subfield specifiers, e.g., ARRAY['001', '245a', '500a'].
3397 The second argument is an array of text containing the values to plug into each field.  
3398 If the value for a given field is NULL or the empty string, it is not inserted.
3399 $$;
3400
3401 CREATE OR REPLACE FUNCTION migration_tools.make_stub_bib (text[], text[], text[], text[]) RETURNS TEXT AS $func$
3402
3403 use strict;
3404 use warnings;
3405
3406 use MARC::Record;
3407 use MARC::File::XML (BinaryEncoding => 'UTF-8');
3408 use Text::CSV;
3409
3410 my $in_tags = shift;
3411 my $in_ind1 = shift;
3412 my $in_ind2 = shift;
3413 my $in_values = shift;
3414
3415 # hack-and-slash parsing of array-passed-as-string;
3416 # this can go away once everybody is running Postgres 9.1+
3417 my $csv = Text::CSV->new({binary => 1});
3418 $in_tags =~ s/^{//;
3419 $in_tags =~ s/}$//;
3420 my $status = $csv->parse($in_tags);
3421 my $tags = [ $csv->fields() ];
3422 $in_ind1 =~ s/^{//;
3423 $in_ind1 =~ s/}$//;
3424 $status = $csv->parse($in_ind1);
3425 my $ind1s = [ $csv->fields() ];
3426 $in_ind2 =~ s/^{//;
3427 $in_ind2 =~ s/}$//;
3428 $status = $csv->parse($in_ind2);
3429 my $ind2s = [ $csv->fields() ];
3430 $in_values =~ s/^{//;
3431 $in_values =~ s/}$//;
3432 $status = $csv->parse($in_values);
3433 my $values = [ $csv->fields() ];
3434
3435 my $marc = MARC::Record->new();
3436
3437 $marc->leader('00000nam a22000007  4500');
3438 $marc->append_fields(MARC::Field->new('008', '000000s                       000   eng d'));
3439
3440 foreach my $i (0..$#$tags) {
3441     my ($tag, $sf);
3442     if ($tags->[$i] =~ /^(\d{3})([0-9a-z])$/) {
3443         $tag = $1;
3444         $sf = $2;
3445         $marc->append_fields(MARC::Field->new($tag, $ind1s->[$i], $ind2s->[$i], $sf => $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
3446     } elsif ($tags->[$i] =~ /^(\d{3})$/) {
3447         $tag = $1;
3448         $marc->append_fields(MARC::Field->new($tag, $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
3449     }
3450 }
3451
3452 my $xml = $marc->as_xml_record;
3453 $xml =~ s/^<\?.+?\?>$//mo;
3454 $xml =~ s/\n//sgo;
3455 $xml =~ s/>\s+</></sgo;
3456
3457 return $xml;
3458
3459 $func$ LANGUAGE PLPERLU;
3460 COMMENT ON FUNCTION migration_tools.make_stub_bib (text[], text[], text[], text[]) IS $$Simple function to create a stub MARCXML bib from a set of columns.
3461 The first argument is an array of tag/subfield specifiers, e.g., ARRAY['001', '245a', '500a'].
3462 The second argument is an array of text containing the values to plug into indicator 1 for each field.  
3463 The third argument is an array of text containing the values to plug into indicator 2 for each field.  
3464 The fourth argument is an array of text containing the values to plug into each field.  
3465 If the value for a given field is NULL or the empty string, it is not inserted.
3466 $$;
3467
3468 CREATE OR REPLACE FUNCTION migration_tools.set_indicator (TEXT, TEXT, INTEGER, CHAR(1)) RETURNS TEXT AS $func$
3469
3470 my ($marcxml, $tag, $pos, $value) = @_;
3471
3472 use MARC::Record;
3473 use MARC::File::XML (BinaryEncoding => 'UTF-8');
3474 use MARC::Charset;
3475 use strict;
3476
3477 MARC::Charset->assume_unicode(1);
3478
3479 elog(ERROR, 'indicator position must be either 1 or 2') unless $pos =~ /^[12]$/;
3480 elog(ERROR, 'MARC tag must be numeric') unless $tag =~ /^\d{3}$/;
3481 elog(ERROR, 'MARC tag must not be control field') if $tag =~ /^00/;
3482 elog(ERROR, 'Value must be exactly one character') unless $value =~ /^.$/;
3483
3484 my $xml = $marcxml;
3485 eval {
3486     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
3487
3488     foreach my $field ($marc->field($tag)) {
3489         $field->update("ind$pos" => $value);
3490     }
3491     $xml = $marc->as_xml_record;
3492     $xml =~ s/^<\?.+?\?>$//mo;
3493     $xml =~ s/\n//sgo;
3494     $xml =~ s/>\s+</></sgo;
3495 };
3496 return $xml;
3497
3498 $func$ LANGUAGE PLPERLU;
3499
3500 COMMENT ON FUNCTION migration_tools.set_indicator(TEXT, TEXT, INTEGER, CHAR(1)) IS $$Set indicator value of a specified MARC field.
3501 The first argument is a MARCXML string.
3502 The second argument is a MARC tag.
3503 The third argument is the indicator position, either 1 or 2.
3504 The fourth argument is the character to set the indicator value to.
3505 All occurences of the specified field will be changed.
3506 The function returns the revised MARCXML string.$$;
3507
3508 CREATE OR REPLACE FUNCTION migration_tools.create_staff_user(
3509     username TEXT,
3510     password TEXT,
3511     org TEXT,
3512     perm_group TEXT,
3513     first_name TEXT DEFAULT '',
3514     last_name TEXT DEFAULT ''
3515 ) RETURNS VOID AS $func$
3516 BEGIN
3517     RAISE NOTICE '%', org ;
3518     INSERT INTO actor.usr (usrname, passwd, ident_type, first_given_name, family_name, home_ou, profile)
3519     SELECT username, password, 1, first_name, last_name, aou.id, pgt.id
3520     FROM   actor.org_unit aou, permission.grp_tree pgt
3521     WHERE  aou.shortname = org
3522     AND    pgt.name = perm_group;
3523 END
3524 $func$
3525 LANGUAGE PLPGSQL;
3526
3527 -- example: SELECT * FROM migration_tools.duplicate_template(5,'{3,4}');
3528 CREATE OR REPLACE FUNCTION migration_tools.duplicate_template (INTEGER, INTEGER[]) RETURNS VOID AS $$
3529     DECLARE
3530         target_event_def ALIAS FOR $1;
3531         orgs ALIAS FOR $2;
3532     BEGIN
3533         DROP TABLE IF EXISTS new_atevdefs;
3534         CREATE TEMP TABLE new_atevdefs (atevdef INTEGER);
3535         FOR i IN array_lower(orgs,1) .. array_upper(orgs,1) LOOP
3536             INSERT INTO action_trigger.event_definition (
3537                 active
3538                 ,owner
3539                 ,name
3540                 ,hook
3541                 ,validator
3542                 ,reactor
3543                 ,cleanup_success
3544                 ,cleanup_failure
3545                 ,delay
3546                 ,max_delay
3547                 ,usr_field
3548                 ,opt_in_setting
3549                 ,delay_field
3550                 ,group_field
3551                 ,template
3552                 ,granularity
3553                 ,repeat_delay
3554             ) SELECT
3555                 'f'
3556                 ,orgs[i]
3557                 ,name || ' (clone of '||target_event_def||')'
3558                 ,hook
3559                 ,validator
3560                 ,reactor
3561                 ,cleanup_success
3562                 ,cleanup_failure
3563                 ,delay
3564                 ,max_delay
3565                 ,usr_field
3566                 ,opt_in_setting
3567                 ,delay_field
3568                 ,group_field
3569                 ,template
3570                 ,granularity
3571                 ,repeat_delay
3572             FROM
3573                 action_trigger.event_definition
3574             WHERE
3575                 id = target_event_def
3576             ;
3577             RAISE INFO 'created atevdef with id = %', currval('action_trigger.event_definition_id_seq');
3578             INSERT INTO new_atevdefs SELECT currval('action_trigger.event_definition_id_seq');
3579             INSERT INTO action_trigger.environment (
3580                 event_def
3581                 ,path
3582                 ,collector
3583                 ,label
3584             ) SELECT
3585                 currval('action_trigger.event_definition_id_seq')
3586                 ,path
3587                 ,collector
3588                 ,label
3589             FROM
3590                 action_trigger.environment
3591             WHERE
3592                 event_def = target_event_def
3593             ;
3594             INSERT INTO action_trigger.event_params (
3595                 event_def
3596                 ,param
3597                 ,value
3598             ) SELECT
3599                 currval('action_trigger.event_definition_id_seq')
3600                 ,param
3601                 ,value
3602             FROM
3603                 action_trigger.event_params
3604             WHERE
3605                 event_def = target_event_def
3606             ;
3607         END LOOP;
3608         RAISE INFO '-- UPDATE action_trigger.event_definition SET active = CASE WHEN id = % THEN FALSE ELSE TRUE END WHERE id in (%,%);', target_event_def, target_event_def, (SELECT array_to_string(array_agg(atevdef),',') from new_atevdefs);
3609     END;
3610 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
3611
3612 -- example: SELECT * FROM migration_tools.duplicate_template_but_change_delay(5,'{3,4}','00:30:00'::INTERVAL);
3613 CREATE OR REPLACE FUNCTION migration_tools.duplicate_template_but_change_delay (INTEGER, INTEGER[], INTERVAL) RETURNS VOID AS $$
3614     DECLARE
3615         target_event_def ALIAS FOR $1;
3616         orgs ALIAS FOR $2;
3617         new_interval ALIAS FOR $3;
3618     BEGIN
3619         DROP TABLE IF EXISTS new_atevdefs;
3620         CREATE TEMP TABLE new_atevdefs (atevdef INTEGER);
3621         FOR i IN array_lower(orgs,1) .. array_upper(orgs,1) LOOP
3622             INSERT INTO action_trigger.event_definition (
3623                 active
3624                 ,owner
3625                 ,name
3626                 ,hook
3627                 ,validator
3628                 ,reactor
3629                 ,cleanup_success
3630                 ,cleanup_failure
3631                 ,delay
3632                 ,max_delay
3633                 ,usr_field
3634                 ,opt_in_setting
3635                 ,delay_field
3636                 ,group_field
3637                 ,template
3638                 ,granularity
3639                 ,repeat_delay
3640             ) SELECT
3641                 'f'
3642                 ,orgs[i]
3643                 ,name || ' (clone of '||target_event_def||')'
3644                 ,hook
3645                 ,validator
3646                 ,reactor
3647                 ,cleanup_success
3648                 ,cleanup_failure
3649                 ,new_interval
3650                 ,max_delay
3651                 ,usr_field
3652                 ,opt_in_setting
3653                 ,delay_field
3654                 ,group_field
3655                 ,template
3656                 ,granularity
3657                 ,repeat_delay
3658             FROM
3659                 action_trigger.event_definition
3660             WHERE
3661                 id = target_event_def
3662             ;
3663             RAISE INFO 'created atevdef with id = %', currval('action_trigger.event_definition_id_seq');
3664             INSERT INTO new_atevdefs SELECT currval('action_trigger.event_definition_id_seq');
3665             INSERT INTO action_trigger.environment (
3666                 event_def
3667                 ,path
3668                 ,collector
3669                 ,label
3670             ) SELECT
3671                 currval('action_trigger.event_definition_id_seq')
3672                 ,path
3673                 ,collector
3674                 ,label
3675             FROM
3676                 action_trigger.environment
3677             WHERE
3678                 event_def = target_event_def
3679             ;
3680             INSERT INTO action_trigger.event_params (
3681                 event_def
3682                 ,param
3683                 ,value
3684             ) SELECT
3685                 currval('action_trigger.event_definition_id_seq')
3686                 ,param
3687                 ,value
3688             FROM
3689                 action_trigger.event_params
3690             WHERE
3691                 event_def = target_event_def
3692             ;
3693         END LOOP;
3694         RAISE INFO '-- UPDATE action_trigger.event_definition SET active = CASE WHEN id = % THEN FALSE ELSE TRUE END WHERE id in (%,%);', target_event_def, target_event_def, (SELECT array_to_string(array_agg(atevdef),',') from new_atevdefs);
3695     END;
3696 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
3697
3698 -- example: SELECT * FROM migration_tools.duplicate_templates(3,'{5,6}');
3699 CREATE OR REPLACE FUNCTION migration_tools.duplicate_templates (INTEGER, INTEGER[]) RETURNS VOID AS $$
3700     DECLARE
3701         org ALIAS FOR $1;
3702         target_event_defs ALIAS FOR $2;
3703     BEGIN
3704         DROP TABLE IF EXISTS new_atevdefs;
3705         CREATE TEMP TABLE new_atevdefs (atevdef INTEGER);
3706         FOR i IN array_lower(target_event_defs,1) .. array_upper(target_event_defs,1) LOOP
3707             INSERT INTO action_trigger.event_definition (
3708                 active
3709                 ,owner
3710                 ,name
3711                 ,hook
3712                 ,validator
3713                 ,reactor
3714                 ,cleanup_success
3715                 ,cleanup_failure
3716                 ,delay
3717                 ,max_delay
3718                 ,usr_field
3719                 ,opt_in_setting
3720                 ,delay_field
3721                 ,group_field
3722                 ,template
3723                 ,granularity
3724                 ,repeat_delay
3725             ) SELECT
3726                 'f'
3727                 ,org
3728                 ,name || ' (clone of '||target_event_defs[i]||')'
3729                 ,hook
3730                 ,validator
3731                 ,reactor
3732                 ,cleanup_success
3733                 ,cleanup_failure
3734                 ,delay
3735                 ,max_delay
3736                 ,usr_field
3737                 ,opt_in_setting
3738                 ,delay_field
3739                 ,group_field
3740                 ,template
3741                 ,granularity
3742                 ,repeat_delay
3743             FROM
3744                 action_trigger.event_definition
3745             WHERE
3746                 id = target_event_defs[i]
3747             ;
3748             RAISE INFO 'created atevdef with id = %', currval('action_trigger.event_definition_id_seq');
3749             INSERT INTO new_atevdefs SELECT currval('action_trigger.event_definition_id_seq');
3750             INSERT INTO action_trigger.environment (
3751                 event_def
3752                 ,path
3753                 ,collector
3754                 ,label
3755             ) SELECT
3756                 currval('action_trigger.event_definition_id_seq')
3757                 ,path
3758                 ,collector
3759                 ,label
3760             FROM
3761                 action_trigger.environment
3762             WHERE
3763                 event_def = target_event_defs[i]
3764             ;
3765             INSERT INTO action_trigger.event_params (
3766                 event_def
3767                 ,param
3768                 ,value
3769             ) SELECT
3770                 currval('action_trigger.event_definition_id_seq')
3771                 ,param
3772                 ,value
3773             FROM
3774                 action_trigger.event_params
3775             WHERE
3776                 event_def = target_event_defs[i]
3777             ;
3778         END LOOP;
3779         RAISE INFO '-- UPDATE action_trigger.event_definition SET active = TRUE WHERE id in (%);', (SELECT array_to_string(array_agg(atevdef),',') from new_atevdefs);
3780     END;
3781 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
3782
3783 CREATE OR REPLACE FUNCTION migration_tools.reset_event (BIGINT) RETURNS VOID AS $$
3784     UPDATE
3785         action_trigger.event
3786     SET
3787          start_time = NULL
3788         ,update_time = NULL
3789         ,complete_time = NULL
3790         ,update_process = NULL
3791         ,state = 'pending'
3792         ,template_output = NULL
3793         ,error_output = NULL
3794         ,async_output = NULL
3795     WHERE
3796         id = $1;
3797 $$ LANGUAGE SQL;
3798
3799 CREATE OR REPLACE FUNCTION migration_tools.get_marc_leader (TEXT) RETURNS TEXT AS $$
3800     my ($marcxml) = @_;
3801
3802     use MARC::Record;
3803     use MARC::File::XML;
3804     use MARC::Field;
3805
3806     my $field;
3807     eval {
3808         my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
3809         $field = $marc->leader();
3810     };
3811     return $field;
3812 $$ LANGUAGE PLPERLU STABLE;
3813
3814 CREATE OR REPLACE FUNCTION migration_tools.get_marc_tag (TEXT, TEXT, TEXT, TEXT) RETURNS TEXT AS $$
3815     my ($marcxml, $tag, $subfield, $delimiter) = @_;
3816
3817     use MARC::Record;
3818     use MARC::File::XML;
3819     use MARC::Field;
3820
3821     my $field;
3822     eval {
3823         my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
3824         $field = $marc->field($tag);
3825     };
3826     return $field->as_string($subfield,$delimiter);
3827 $$ LANGUAGE PLPERLU STABLE;
3828
3829 CREATE OR REPLACE FUNCTION migration_tools.get_marc_tags (TEXT, TEXT, TEXT, TEXT) RETURNS TEXT[] AS $$
3830     my ($marcxml, $tag, $subfield, $delimiter) = @_;
3831
3832     use MARC::Record;
3833     use MARC::File::XML;
3834     use MARC::Field;
3835
3836     my @fields;
3837     eval {
3838         my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
3839         @fields = $marc->field($tag);
3840     };
3841     my @texts;
3842     foreach my $field (@fields) {
3843         push @texts, $field->as_string($subfield,$delimiter);
3844     }
3845     return \@texts;
3846 $$ LANGUAGE PLPERLU STABLE;
3847
3848 CREATE OR REPLACE FUNCTION migration_tools.get_marc_tags_filtered (TEXT, TEXT, TEXT, TEXT, TEXT) RETURNS TEXT[] AS $$
3849     my ($marcxml, $tag, $subfield, $delimiter, $match) = @_;
3850
3851     use MARC::Record;
3852     use MARC::File::XML;
3853     use MARC::Field;
3854
3855     my @fields;
3856     eval {
3857         my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
3858         @fields = $marc->field($tag);
3859     };
3860     my @texts;
3861     foreach my $field (@fields) {
3862         if ($field->as_string() =~ qr/$match/) {
3863             push @texts, $field->as_string($subfield,$delimiter);
3864         }
3865     }
3866     return \@texts;
3867 $$ LANGUAGE PLPERLU STABLE;
3868
3869 CREATE OR REPLACE FUNCTION migration_tools.find_hold_matrix_matchpoint (INTEGER) RETURNS INTEGER AS $$
3870     SELECT action.find_hold_matrix_matchpoint(
3871         (SELECT pickup_lib FROM action.hold_request WHERE id = $1),
3872         (SELECT request_lib FROM action.hold_request WHERE id = $1),
3873         (SELECT current_copy FROM action.hold_request WHERE id = $1),
3874         (SELECT usr FROM action.hold_request WHERE id = $1),
3875         (SELECT requestor FROM action.hold_request WHERE id = $1)
3876     );
3877 $$ LANGUAGE SQL;
3878
3879 CREATE OR REPLACE FUNCTION migration_tools.find_hold_matrix_matchpoint2 (INTEGER) RETURNS SETOF action.matrix_test_result AS $$
3880     SELECT action.hold_request_permit_test(
3881         (SELECT pickup_lib FROM action.hold_request WHERE id = $1),
3882         (SELECT request_lib FROM action.hold_request WHERE id = $1),
3883         (SELECT current_copy FROM action.hold_request WHERE id = $1),
3884         (SELECT usr FROM action.hold_request WHERE id = $1),
3885         (SELECT requestor FROM action.hold_request WHERE id = $1)
3886     );
3887 $$ LANGUAGE SQL;
3888
3889 CREATE OR REPLACE FUNCTION migration_tools.find_circ_matrix_matchpoint (INTEGER) RETURNS SETOF action.found_circ_matrix_matchpoint AS $$
3890     SELECT action.find_circ_matrix_matchpoint(
3891         (SELECT circ_lib FROM action.circulation WHERE id = $1),
3892         (SELECT target_copy FROM action.circulation WHERE id = $1),
3893         (SELECT usr FROM action.circulation WHERE id = $1),
3894         (SELECT COALESCE(
3895                 NULLIF(phone_renewal,false),
3896                 NULLIF(desk_renewal,false),
3897                 NULLIF(opac_renewal,false),
3898                 false
3899             ) FROM action.circulation WHERE id = $1
3900         )
3901     );
3902 $$ LANGUAGE SQL;
3903
3904 CREATE OR REPLACE FUNCTION migration_tools.assert (BOOLEAN) RETURNS VOID AS $$
3905     DECLARE
3906         test ALIAS FOR $1;
3907     BEGIN
3908         IF NOT test THEN
3909             RAISE EXCEPTION 'assertion';
3910         END IF;
3911     END;
3912 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
3913
3914 CREATE OR REPLACE FUNCTION migration_tools.assert (BOOLEAN,TEXT) RETURNS VOID AS $$
3915     DECLARE
3916         test ALIAS FOR $1;
3917         msg ALIAS FOR $2;
3918     BEGIN
3919         IF NOT test THEN
3920             RAISE EXCEPTION '%', msg;
3921         END IF;
3922     END;
3923 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
3924
3925 CREATE OR REPLACE FUNCTION migration_tools.assert (BOOLEAN,TEXT,TEXT) RETURNS TEXT AS $$
3926     DECLARE
3927         test ALIAS FOR $1;
3928         fail_msg ALIAS FOR $2;
3929         success_msg ALIAS FOR $3;
3930     BEGIN
3931         IF NOT test THEN
3932             RAISE EXCEPTION '%', fail_msg;
3933         END IF;
3934         RETURN success_msg;
3935     END;
3936 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
3937
3938 -- push bib sequence and return starting value for reserved range
3939 CREATE OR REPLACE FUNCTION migration_tools.push_bib_sequence(INTEGER) RETURNS BIGINT AS $$
3940     DECLARE
3941         bib_count ALIAS FOR $1;
3942         output BIGINT;
3943     BEGIN
3944         PERFORM setval('biblio.record_entry_id_seq',(SELECT MAX(id) FROM biblio.record_entry) + bib_count + 2000);
3945         FOR output IN
3946             SELECT CEIL(MAX(id)/1000)*1000+1000 FROM biblio.record_entry WHERE id < (SELECT last_value FROM biblio.record_entry_id_seq)
3947         LOOP
3948             RETURN output;
3949         END LOOP;
3950     END;
3951 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
3952
3953 -- set a new salted password
3954
3955 CREATE OR REPLACE FUNCTION migration_tools.set_salted_passwd(INTEGER,TEXT) RETURNS BOOLEAN AS $$
3956     DECLARE
3957         usr_id              ALIAS FOR $1;
3958         plain_passwd        ALIAS FOR $2;
3959         plain_salt          TEXT;
3960         md5_passwd          TEXT;
3961     BEGIN
3962
3963         SELECT actor.create_salt('main') INTO plain_salt;
3964
3965         SELECT MD5(plain_passwd) INTO md5_passwd;
3966         
3967         PERFORM actor.set_passwd(usr_id, 'main', MD5(plain_salt || md5_passwd), plain_salt);
3968
3969         RETURN TRUE;
3970
3971     END;
3972 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
3973
3974
3975 -- convenience functions for handling copy_location maps
3976 CREATE OR REPLACE FUNCTION migration_tools.handle_shelf (TEXT,TEXT,TEXT,INTEGER) RETURNS VOID AS $$
3977     SELECT migration_tools._handle_shelf($1,$2,$3,$4,TRUE);
3978 $$ LANGUAGE SQL;
3979
3980 CREATE OR REPLACE FUNCTION migration_tools._handle_shelf (TEXT,TEXT,TEXT,INTEGER,BOOLEAN) RETURNS VOID AS $$
3981     DECLARE
3982         table_schema ALIAS FOR $1;
3983         table_name ALIAS FOR $2;
3984         org_shortname ALIAS FOR $3;
3985         org_range ALIAS FOR $4;
3986         make_assertion ALIAS FOR $5;
3987         proceed BOOLEAN;
3988         org INTEGER;
3989         -- if x_org is on the mapping table, it'll take precedence over the passed org_shortname param
3990         -- though we'll still use the passed org for the full path traversal when needed
3991         x_org_found BOOLEAN;
3992         x_org INTEGER;
3993         org_list INTEGER[];
3994         o INTEGER;
3995         row_count NUMERIC;
3996     BEGIN
3997         EXECUTE 'SELECT EXISTS (
3998             SELECT 1
3999             FROM information_schema.columns
4000             WHERE table_schema = $1
4001             AND table_name = $2
4002             and column_name = ''desired_shelf''
4003         )' INTO proceed USING table_schema, table_name;
4004         IF NOT proceed THEN
4005             RAISE EXCEPTION 'Missing column desired_shelf';
4006         END IF;
4007
4008         EXECUTE 'SELECT EXISTS (
4009             SELECT 1
4010             FROM information_schema.columns
4011             WHERE table_schema = $1
4012             AND table_name = $2
4013             and column_name = ''x_org''
4014         )' INTO x_org_found USING table_schema, table_name;
4015
4016         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
4017         IF org IS NULL THEN
4018             RAISE EXCEPTION 'Cannot find org by shortname';
4019         END IF;
4020
4021         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
4022
4023         EXECUTE 'ALTER TABLE '
4024             || quote_ident(table_name)
4025             || ' DROP COLUMN IF EXISTS x_shelf';
4026         EXECUTE 'ALTER TABLE '
4027             || quote_ident(table_name)
4028             || ' ADD COLUMN x_shelf INTEGER';
4029
4030         IF x_org_found THEN
4031             RAISE INFO 'Found x_org column';
4032             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4033                 || ' SET x_shelf = b.id FROM asset_copy_location b'
4034                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
4035                 || ' AND b.owning_lib = x_org'
4036                 || ' AND NOT b.deleted';
4037             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4038                 || ' SET x_shelf = b.id FROM asset.copy_location b'
4039                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
4040                 || ' AND b.owning_lib = x_org'
4041                 || ' AND x_shelf IS NULL'
4042                 || ' AND NOT b.deleted';
4043         ELSE
4044             RAISE INFO 'Did not find x_org column';
4045             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4046                 || ' SET x_shelf = b.id FROM asset_copy_location b'
4047                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
4048                 || ' AND b.owning_lib = $1'
4049                 || ' AND NOT b.deleted'
4050             USING org;
4051             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4052                 || ' SET x_shelf = b.id FROM asset_copy_location b'
4053                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
4054                 || ' AND b.owning_lib = $1'
4055                 || ' AND x_shelf IS NULL'
4056                 || ' AND NOT b.deleted'
4057             USING org;
4058         END IF;
4059
4060         FOREACH o IN ARRAY org_list LOOP
4061             RAISE INFO 'Considering org %', o;
4062             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4063                 || ' SET x_shelf = b.id FROM asset.copy_location b'
4064                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
4065                 || ' AND b.owning_lib = $1 AND x_shelf IS NULL'
4066                 || ' AND NOT b.deleted'
4067             USING o;
4068             GET DIAGNOSTICS row_count = ROW_COUNT;
4069             RAISE INFO 'Updated % rows', row_count;
4070         END LOOP;
4071
4072         IF make_assertion THEN
4073             EXECUTE 'SELECT migration_tools.assert(
4074                 NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_shelf <> '''' AND x_shelf IS NULL),
4075                 ''Cannot find a desired location'',
4076                 ''Found all desired locations''
4077             );';
4078         END IF;
4079
4080     END;
4081 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
4082
4083 -- convenience functions for handling circmod maps
4084
4085 CREATE OR REPLACE FUNCTION migration_tools.handle_circmod (TEXT,TEXT) RETURNS VOID AS $$
4086     DECLARE
4087         table_schema ALIAS FOR $1;
4088         table_name ALIAS FOR $2;
4089         proceed BOOLEAN;
4090     BEGIN
4091         EXECUTE 'SELECT EXISTS (
4092             SELECT 1
4093             FROM information_schema.columns
4094             WHERE table_schema = $1
4095             AND table_name = $2
4096             and column_name = ''desired_circmod''
4097         )' INTO proceed USING table_schema, table_name;
4098         IF NOT proceed THEN
4099             RAISE EXCEPTION 'Missing column desired_circmod'; 
4100         END IF;
4101
4102         EXECUTE 'ALTER TABLE '
4103             || quote_ident(table_name)
4104             || ' DROP COLUMN IF EXISTS x_circmod';
4105         EXECUTE 'ALTER TABLE '
4106             || quote_ident(table_name)
4107             || ' ADD COLUMN x_circmod TEXT';
4108
4109         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4110             || ' SET x_circmod = code FROM config.circ_modifier b'
4111             || ' WHERE BTRIM(UPPER(a.desired_circmod)) = BTRIM(UPPER(b.code))';
4112
4113         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4114             || ' SET x_circmod = code FROM config.circ_modifier b'
4115             || ' WHERE BTRIM(UPPER(a.desired_circmod)) = BTRIM(UPPER(b.name))'
4116             || ' AND x_circmod IS NULL';
4117
4118         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4119             || ' SET x_circmod = code FROM config.circ_modifier b'
4120             || ' WHERE BTRIM(UPPER(a.desired_circmod)) = BTRIM(UPPER(b.description))'
4121             || ' AND x_circmod IS NULL';
4122
4123         EXECUTE 'SELECT migration_tools.assert(
4124             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_circmod <> '''' AND x_circmod IS NULL),
4125             ''Cannot find a desired circulation modifier'',
4126             ''Found all desired circulation modifiers''
4127         );';
4128
4129     END;
4130 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
4131
4132 -- convenience functions for handling item status maps
4133
4134 CREATE OR REPLACE FUNCTION migration_tools.handle_status (TEXT,TEXT) RETURNS VOID AS $$
4135     DECLARE
4136         table_schema ALIAS FOR $1;
4137         table_name ALIAS FOR $2;
4138         proceed BOOLEAN;
4139     BEGIN
4140         EXECUTE 'SELECT EXISTS (
4141             SELECT 1
4142             FROM information_schema.columns
4143             WHERE table_schema = $1
4144             AND table_name = $2
4145             and column_name = ''desired_status''
4146         )' INTO proceed USING table_schema, table_name;
4147         IF NOT proceed THEN
4148             RAISE EXCEPTION 'Missing column desired_status'; 
4149         END IF;
4150
4151         EXECUTE 'ALTER TABLE '
4152             || quote_ident(table_name)
4153             || ' DROP COLUMN IF EXISTS x_status';
4154         EXECUTE 'ALTER TABLE '
4155             || quote_ident(table_name)
4156             || ' ADD COLUMN x_status INTEGER';
4157
4158         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4159             || ' SET x_status = id FROM config.copy_status b'
4160             || ' WHERE BTRIM(UPPER(a.desired_status)) = BTRIM(UPPER(b.name))';
4161
4162         EXECUTE 'SELECT migration_tools.assert(
4163             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_status <> '''' AND x_status IS NULL),
4164             ''Cannot find a desired copy status'',
4165             ''Found all desired copy statuses''
4166         );';
4167
4168     END;
4169 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
4170
4171 -- convenience functions for handling org maps
4172
4173 CREATE OR REPLACE FUNCTION migration_tools.handle_org (TEXT,TEXT) RETURNS VOID AS $$
4174     DECLARE
4175         table_schema ALIAS FOR $1;
4176         table_name ALIAS FOR $2;
4177         proceed BOOLEAN;
4178     BEGIN
4179         EXECUTE 'SELECT EXISTS (
4180             SELECT 1
4181             FROM information_schema.columns
4182             WHERE table_schema = $1
4183             AND table_name = $2
4184             and column_name = ''desired_org''
4185         )' INTO proceed USING table_schema, table_name;
4186         IF NOT proceed THEN
4187             RAISE EXCEPTION 'Missing column desired_org'; 
4188         END IF;
4189
4190         EXECUTE 'ALTER TABLE '
4191             || quote_ident(table_name)
4192             || ' DROP COLUMN IF EXISTS x_org';
4193         EXECUTE 'ALTER TABLE '
4194             || quote_ident(table_name)
4195             || ' ADD COLUMN x_org INTEGER';
4196
4197         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4198             || ' SET x_org = b.id FROM actor.org_unit b'
4199             || ' WHERE BTRIM(a.desired_org) = BTRIM(b.shortname)';
4200
4201         EXECUTE 'SELECT migration_tools.assert(
4202             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_org <> '''' AND x_org IS NULL),
4203             ''Cannot find a desired org unit'',
4204             ''Found all desired org units''
4205         );';
4206
4207     END;
4208 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
4209
4210 -- convenience function for handling desired_not_migrate
4211
4212 CREATE OR REPLACE FUNCTION migration_tools.handle_not_migrate (TEXT,TEXT) RETURNS VOID AS $$
4213     DECLARE
4214         table_schema ALIAS FOR $1;
4215         table_name ALIAS FOR $2;
4216         proceed BOOLEAN;
4217     BEGIN
4218         EXECUTE 'SELECT EXISTS (
4219             SELECT 1
4220             FROM information_schema.columns
4221             WHERE table_schema = $1
4222             AND table_name = $2
4223             and column_name = ''desired_not_migrate''
4224         )' INTO proceed USING table_schema, table_name;
4225         IF NOT proceed THEN
4226             RAISE EXCEPTION 'Missing column desired_not_migrate'; 
4227         END IF;
4228
4229         EXECUTE 'ALTER TABLE '
4230             || quote_ident(table_name)
4231             || ' DROP COLUMN IF EXISTS x_migrate';
4232         EXECUTE 'ALTER TABLE '
4233             || quote_ident(table_name)
4234             || ' ADD COLUMN x_migrate BOOLEAN';
4235
4236         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4237             || ' SET x_migrate = CASE'
4238             || ' WHEN BTRIM(desired_not_migrate) = ''TRUE'' THEN FALSE'
4239             || ' WHEN BTRIM(desired_not_migrate) = ''DNM'' THEN FALSE'
4240             || ' WHEN BTRIM(desired_not_migrate) = ''Do Not Migrate'' THEN FALSE'
4241             || ' WHEN BTRIM(desired_not_migrate) = ''FALSE'' THEN TRUE'
4242             || ' WHEN BTRIM(desired_not_migrate) = ''Migrate'' THEN TRUE'
4243             || ' WHEN BTRIM(desired_not_migrate) = '''' THEN TRUE'
4244             || ' END';
4245
4246         EXECUTE 'SELECT migration_tools.assert(
4247             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE x_migrate IS NULL),
4248             ''Not all desired_not_migrate values understood'',
4249             ''All desired_not_migrate values understood''
4250         );';
4251
4252     END;
4253 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
4254
4255 -- convenience function for handling desired_not_migrate
4256
4257 CREATE OR REPLACE FUNCTION migration_tools.handle_barred_or_blocked (TEXT,TEXT) RETURNS VOID AS $$
4258     DECLARE
4259         table_schema ALIAS FOR $1;
4260         table_name ALIAS FOR $2;
4261         proceed BOOLEAN;
4262     BEGIN
4263         EXECUTE 'SELECT EXISTS (
4264             SELECT 1
4265             FROM information_schema.columns
4266             WHERE table_schema = $1
4267             AND table_name = $2
4268             and column_name = ''desired_barred_or_blocked''
4269         )' INTO proceed USING table_schema, table_name;
4270         IF NOT proceed THEN
4271             RAISE EXCEPTION 'Missing column desired_barred_or_blocked'; 
4272         END IF;
4273
4274         EXECUTE 'ALTER TABLE '
4275             || quote_ident(table_name)
4276             || ' DROP COLUMN IF EXISTS x_barred';
4277         EXECUTE 'ALTER TABLE '
4278             || quote_ident(table_name)
4279             || ' ADD COLUMN x_barred BOOLEAN';
4280
4281         EXECUTE 'ALTER TABLE '
4282             || quote_ident(table_name)
4283             || ' DROP COLUMN IF EXISTS x_blocked';
4284         EXECUTE 'ALTER TABLE '
4285             || quote_ident(table_name)
4286             || ' ADD COLUMN x_blocked BOOLEAN';
4287
4288         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4289             || ' SET x_barred = CASE'
4290             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Barred'' THEN TRUE'
4291             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Blocked'' THEN FALSE'
4292             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Neither'' THEN FALSE'
4293             || ' WHEN BTRIM(desired_barred_or_blocked) = '''' THEN FALSE'
4294             || ' END';
4295
4296         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4297             || ' SET x_blocked = CASE'
4298             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Blocked'' THEN TRUE'
4299             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Barred'' THEN FALSE'
4300             || ' WHEN BTRIM(desired_barred_or_blocked) = ''Neither'' THEN FALSE'
4301             || ' WHEN BTRIM(desired_barred_or_blocked) = '''' THEN FALSE'
4302             || ' END';
4303
4304         EXECUTE 'SELECT migration_tools.assert(
4305             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE x_barred IS NULL or x_blocked IS NULL),
4306             ''Not all desired_barred_or_blocked values understood'',
4307             ''All desired_barred_or_blocked values understood''
4308         );';
4309
4310     END;
4311 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
4312
4313 -- convenience function for handling desired_profile
4314
4315 CREATE OR REPLACE FUNCTION migration_tools.handle_profile (TEXT,TEXT) RETURNS VOID AS $$
4316     DECLARE
4317         table_schema ALIAS FOR $1;
4318         table_name ALIAS FOR $2;
4319         proceed BOOLEAN;
4320     BEGIN
4321         EXECUTE 'SELECT EXISTS (
4322             SELECT 1
4323             FROM information_schema.columns
4324             WHERE table_schema = $1
4325             AND table_name = $2
4326             and column_name = ''desired_profile''
4327         )' INTO proceed USING table_schema, table_name;
4328         IF NOT proceed THEN
4329             RAISE EXCEPTION 'Missing column desired_profile'; 
4330         END IF;
4331
4332         EXECUTE 'ALTER TABLE '
4333             || quote_ident(table_name)
4334             || ' DROP COLUMN IF EXISTS x_profile';
4335         EXECUTE 'ALTER TABLE '
4336             || quote_ident(table_name)
4337             || ' ADD COLUMN x_profile INTEGER';
4338
4339         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
4340             || ' SET x_profile = b.id FROM permission.grp_tree b'
4341             || ' WHERE BTRIM(UPPER(a.desired_profile)) = BTRIM(UPPER(b.name))';
4342
4343         EXECUTE 'SELECT migration_tools.assert(
4344             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_profile <> '''' AND x_profile IS NULL),
4345             ''Cannot find a desired profile'',
4346             ''Found all desired profiles''
4347         );';
4348
4349     END;
4350 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
4351
4352 -- convenience function for handling desired actor stat cats
4353
4354 CREATE OR REPLACE FUNCTION migration_tools.vivicate_actor_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
4355     DECLARE
4356         table_schema ALIAS FOR $1;
4357         table_name ALIAS FOR $2;
4358         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
4359         org_shortname ALIAS FOR $4;
4360         proceed BOOLEAN;
4361         org INTEGER;
4362         org_list INTEGER[];
4363         sc TEXT;
4364         sce TEXT;
4365     BEGIN
4366
4367         SELECT 'desired_sc' || field_suffix INTO sc;
4368         SELECT 'desired_sce' || field_suffix INTO sce;
4369
4370         EXECUTE 'SELECT EXISTS (
4371             SELECT 1
4372             FROM information_schema.columns
4373             WHERE table_schema = $1
4374             AND table_name = $2
4375             and column_name = $3
4376         )' INTO proceed USING table_schema, table_name, sc;
4377         IF NOT proceed THEN
4378             RAISE EXCEPTION 'Missing column %', sc; 
4379         END IF;
4380         EXECUTE 'SELECT EXISTS (
4381             SELECT 1
4382             FROM information_schema.columns
4383             WHERE table_schema = $1
4384             AND table_name = $2
4385             and column_name = $3
4386         )' INTO proceed USING table_schema, table_name, sce;
4387         IF NOT proceed THEN
4388             RAISE EXCEPTION 'Missing column %', sce; 
4389         END IF;
4390
4391         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
4392         IF org IS NULL THEN
4393             RAISE EXCEPTION 'Cannot find org by shortname';
4394         END IF;
4395         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
4396
4397         -- caller responsible for their own truncates though we try to prevent duplicates
4398         EXECUTE 'INSERT INTO actor_stat_cat (owner, name)
4399             SELECT DISTINCT
4400                  $1
4401                 ,BTRIM('||sc||')
4402             FROM 
4403                 ' || quote_ident(table_name) || '
4404             WHERE
4405                 NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
4406                 AND NOT EXISTS (
4407                     SELECT id
4408                     FROM actor.stat_cat
4409                     WHERE owner = ANY ($2)
4410                     AND name = BTRIM('||sc||')
4411                 )
4412                 AND NOT EXISTS (
4413                     SELECT id
4414                     FROM actor_stat_cat
4415                     WHERE owner = ANY ($2)
4416                     AND name = BTRIM('||sc||')
4417                 )
4418             ORDER BY 2;'
4419         USING org, org_list;
4420
4421         EXECUTE 'INSERT INTO actor_stat_cat_entry (stat_cat, owner, value)
4422             SELECT DISTINCT
4423                 COALESCE(
4424                     (SELECT id
4425                         FROM actor.stat_cat
4426                         WHERE owner = ANY ($2)
4427                         AND BTRIM('||sc||') = BTRIM(name))
4428                    ,(SELECT id
4429                         FROM actor_stat_cat
4430                         WHERE owner = ANY ($2)
4431                         AND BTRIM('||sc||') = BTRIM(name))
4432                 )
4433                 ,$1
4434                 ,BTRIM('||sce||')
4435             FROM 
4436                 ' || quote_ident(table_name) || '
4437             WHERE
4438                     NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
4439                 AND NULLIF(BTRIM('||sce||'),'''') IS NOT NULL
4440                 AND NOT EXISTS (
4441                     SELECT id
4442                     FROM actor.stat_cat_entry
4443                     WHERE stat_cat = (
4444                         SELECT id
4445                         FROM actor.stat_cat
4446                         WHERE owner = ANY ($2)
4447                         AND BTRIM('||sc||') = BTRIM(name)
4448                     ) AND value = BTRIM('||sce||')
4449                     AND owner = ANY ($2)
4450                 )
4451                 AND NOT EXISTS (
4452                     SELECT id
4453                     FROM actor_stat_cat_entry
4454                     WHERE stat_cat = (
4455                         SELECT id
4456                         FROM actor_stat_cat
4457                         WHERE owner = ANY ($2)
4458                         AND BTRIM('||sc||') = BTRIM(name)
4459                     ) AND value = BTRIM('||sce||')
4460                     AND owner = ANY ($2)
4461                 )
4462             ORDER BY 1,3;'
4463         USING org, org_list;
4464     END;
4465 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
4466
4467 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
4468     DECLARE
4469         table_schema ALIAS FOR $1;
4470         table_name ALIAS FOR $2;
4471         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
4472         org_shortname ALIAS FOR $4;
4473         proceed BOOLEAN;
4474         org INTEGER;
4475         org_list INTEGER[];
4476         o INTEGER;
4477         sc TEXT;
4478         sce TEXT;
4479     BEGIN
4480         SELECT 'desired_sc' || field_suffix INTO sc;
4481         SELECT 'desired_sce' || field_suffix INTO sce;
4482         EXECUTE 'SELECT EXISTS (
4483             SELECT 1
4484             FROM information_schema.columns
4485             WHERE table_schema = $1
4486             AND table_name = $2
4487             and column_name = $3
4488         )' INTO proceed USING table_schema, table_name, sc;
4489         IF NOT proceed THEN
4490             RAISE EXCEPTION 'Missing column %', sc; 
4491         END IF;
4492         EXECUTE 'SELECT EXISTS (
4493             SELECT 1
4494             FROM information_schema.columns
4495             WHERE table_schema = $1
4496             AND table_name = $2
4497             and column_name = $3
4498         )' INTO proceed USING table_schema, table_name, sce;
4499         IF NOT proceed THEN
4500             RAISE EXCEPTION 'Missing column %', sce; 
4501         END IF;
4502
4503         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
4504         IF org IS NULL THEN
4505             RAISE EXCEPTION 'Cannot find org by shortname';
4506         END IF;
4507
4508         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
4509
4510         EXECUTE 'ALTER TABLE '
4511             || quote_ident(table_name)
4512             || ' DROP COLUMN IF EXISTS x_sc' || field_suffix;
4513         EXECUTE 'ALTER TABLE '
4514             || quote_ident(table_name)
4515             || ' ADD COLUMN x_sc' || field_suffix || ' INTEGER';
4516         EXECUTE 'ALTER TABLE '
4517             || quote_ident(table_name)
4518             || ' DROP COLUMN IF EXISTS x_sce' || field_suffix;
4519         EXECUTE 'ALTER TABLE '
4520             || quote_ident(table_name)
4521             || ' ADD COLUMN x_sce' || field_suffix || ' INTEGER';
4522
4523
4524         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
4525             SET
4526                 x_sc' || field_suffix || ' = id
4527             FROM
4528                 (SELECT id, name, owner FROM actor_stat_cat
4529                     UNION SELECT id, name, owner FROM actor.stat_cat) u
4530             WHERE
4531                     BTRIM(UPPER(u.name)) = BTRIM(UPPER(' || sc || '))
4532                 AND u.owner = ANY ($1);'
4533         USING org_list;
4534
4535         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
4536             SET
4537                 x_sce' || field_suffix || ' = id
4538             FROM
4539                 (SELECT id, stat_cat, owner, value FROM actor_stat_cat_entry
4540                     UNION SELECT id, stat_cat, owner, value FROM actor.stat_cat_entry) u
4541             WHERE
4542                     u.stat_cat = x_sc' || field_suffix || '
4543                 AND BTRIM(UPPER(u.value)) = BTRIM(UPPER(' || sce || '))
4544                 AND u.owner = ANY ($1);'
4545         USING org_list;
4546
4547         EXECUTE 'SELECT migration_tools.assert(
4548             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sc' || field_suffix || ' <> '''' AND x_sc' || field_suffix || ' IS NULL),
4549             ''Cannot find a desired stat cat'',
4550             ''Found all desired stat cats''
4551         );';
4552
4553         EXECUTE 'SELECT migration_tools.assert(
4554             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sce' || field_suffix || ' <> '''' AND x_sce' || field_suffix || ' IS NULL),
4555             ''Cannot find a desired stat cat entry'',
4556             ''Found all desired stat cat entries''
4557         );';
4558
4559     END;
4560 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
4561
4562 -- convenience functions for adding shelving locations
4563 DROP FUNCTION IF EXISTS migration_tools.find_shelf(INT,TEXT);
4564 CREATE OR REPLACE FUNCTION migration_tools.find_shelf(org_id INT, shelf_name TEXT) RETURNS INTEGER AS $$
4565 DECLARE
4566     return_id   INT;
4567     d           INT;
4568     cur_id      INT;
4569 BEGIN
4570     SELECT INTO d MAX(distance) FROM actor.org_unit_ancestors_distance(org_id);
4571     WHILE d >= 0
4572     LOOP
4573         SELECT INTO cur_id id FROM actor.org_unit_ancestor_at_depth(org_id,d);
4574         SELECT INTO return_id id FROM asset.copy_location WHERE owning_lib = cur_id AND name ILIKE shelf_name;
4575         IF return_id IS NOT NULL THEN
4576                 RETURN return_id;
4577         END IF;
4578         d := d - 1;
4579     END LOOP;
4580
4581     RETURN NULL;
4582 END
4583 $$ LANGUAGE plpgsql;
4584
4585 -- may remove later but testing using this with new migration scripts and not loading acls until go live
4586
4587 DROP FUNCTION IF EXISTS migration_tools.find_mig_shelf(INT,TEXT);
4588 CREATE OR REPLACE FUNCTION migration_tools.find_mig_shelf(org_id INT, shelf_name TEXT) RETURNS INTEGER AS $$
4589 DECLARE
4590     return_id   INT;
4591     d           INT;
4592     cur_id      INT;
4593 BEGIN
4594     SELECT INTO d MAX(distance) FROM actor.org_unit_ancestors_distance(org_id);
4595     WHILE d >= 0
4596     LOOP
4597         SELECT INTO cur_id id FROM actor.org_unit_ancestor_at_depth(org_id,d);
4598         
4599         SELECT INTO return_id id FROM 
4600             (SELECT * FROM asset.copy_location UNION ALL SELECT * FROM asset_copy_location) x
4601             WHERE owning_lib = cur_id AND name ILIKE shelf_name;
4602         IF return_id IS NOT NULL THEN
4603                 RETURN return_id;
4604         END IF;
4605         d := d - 1;
4606     END LOOP;
4607
4608     RETURN NULL;
4609 END
4610 $$ LANGUAGE plpgsql;
4611
4612 DROP FUNCTION IF EXISTS migration_tools.munge_sf9(INTEGER,TEXT,TEXT);
4613 CREATE OR REPLACE FUNCTION migration_tools.merge_group(bib_id INTEGER,new_sf9 TEXT,force TEXT DEFAULT 'false')
4614     RETURNS BOOLEAN AS 
4615 $BODY$
4616 DECLARE
4617         marc_xml        TEXT;
4618         new_marc        TEXT;
4619 BEGIN
4620         SELECT marc FROM biblio.record_entry WHERE id = bib_id INTO marc_xml;
4621         
4622         SELECT munge_sf9(marc_xml,new_sf9,force) INTO new_marc;
4623         UPDATE biblio.record_entry SET marc = new_marc WHERE id = bib_id;
4624         
4625         RETURN true;
4626 END;
4627 $BODY$ LANGUAGE plpgsql;
4628
4629 DROP FUNCTION IF EXISTS migration_tools.munge_sf9(TEXT,TEXT,TEXT);
4630 CREATE OR REPLACE FUNCTION migration_tools.munge_sf9(marc_xml TEXT, new_9_to_set TEXT, force TEXT)
4631  RETURNS TEXT
4632  LANGUAGE plperlu
4633 AS $function$
4634 use strict;
4635 use warnings;
4636
4637 use MARC::Record;
4638 use MARC::File::XML (BinaryEncoding => 'utf8');
4639
4640 binmode(STDERR, ':bytes');
4641 binmode(STDOUT, ':utf8');
4642 binmode(STDERR, ':utf8');
4643
4644 my $marc_xml = shift;
4645 my $new_9_to_set = shift;
4646 my $force = shift;
4647
4648 $marc_xml =~ s/(<leader>.........)./${1}a/;
4649
4650 eval {
4651     $marc_xml = MARC::Record->new_from_xml($marc_xml);
4652 };
4653 if ($@) {
4654     #elog("could not parse $bibid: $@\n");
4655     import MARC::File::XML (BinaryEncoding => 'utf8');
4656     return $marc_xml;
4657 }
4658
4659 my @uris = $marc_xml->field('856');
4660 return $marc_xml->as_xml_record() unless @uris;
4661
4662 foreach my $field (@uris) {
4663     my $ind1 = $field->indicator('1');
4664     if (!defined $ind1) { next; }
4665     if ($ind1 ne '1' && $ind1 ne '4' && $force eq 'false') { next; }
4666         if ($ind1 ne '1' && $ind1 ne '4' && $force eq 'true') { $field->set_indicator(1,'4'); }
4667     my $ind2 = $field->indicator('2');
4668     if (!defined $ind2) { next; }
4669     if ($ind2 ne '0' && $ind2 ne '1' && $force eq 'false') { next; }
4670     if ($ind2 ne '0' && $ind2 ne '1' && $force eq 'true') { $field->set_indicator(2,'0'); }
4671     $field->add_subfields( '9' => $new_9_to_set );
4672 }
4673
4674 return $marc_xml->as_xml_record();
4675
4676 $function$;
4677
4678 DROP FUNCTION IF EXISTS migration_tools.munge_sf9_qualifying_match(TEXT,TEXT,TEXT);
4679 CREATE OR REPLACE FUNCTION migration_tools.munge_sf9_qualifying_match(marc_xml TEXT, qualifying_match TEXT, new_9_to_set TEXT, force TEXT)
4680  RETURNS TEXT
4681  LANGUAGE plperlu
4682 AS $function$
4683 use strict;
4684 use warnings;
4685
4686 use MARC::Record;
4687 use MARC::File::XML (BinaryEncoding => 'utf8');
4688
4689 binmode(STDERR, ':bytes');
4690 binmode(STDOUT, ':utf8');
4691 binmode(STDERR, ':utf8');
4692
4693 my $marc_xml = shift;
4694 my $qualifying_match = shift;
4695 my $new_9_to_set = shift;
4696 my $force = shift;
4697
4698 $marc_xml =~ s/(<leader>.........)./${1}a/;
4699
4700 eval {
4701     $marc_xml = MARC::Record->new_from_xml($marc_xml);
4702 };
4703 if ($@) {
4704     #elog("could not parse $bibid: $@\n");
4705     import MARC::File::XML (BinaryEncoding => 'utf8');
4706     return $marc_xml;
4707 }
4708
4709 my @uris = $marc_xml->field('856');
4710 return $marc_xml->as_xml_record() unless @uris;
4711
4712 foreach my $field (@uris) {
4713     if ($field->as_string() =~ qr/$qualifying_match/) {
4714         my $ind1 = $field->indicator('1');
4715         if (!defined $ind1) { next; }
4716         if ($ind1 ne '1' && $ind1 ne '4' && $force eq 'false') { next; }
4717         if ($ind1 ne '1' && $ind1 ne '4' && $force eq 'true') { $field->set_indicator(1,'4'); }
4718         my $ind2 = $field->indicator('2');
4719         if (!defined $ind2) { next; }
4720         if ($ind2 ne '0' && $ind2 ne '1' && $force eq 'false') { next; }
4721         if ($ind2 ne '0' && $ind2 ne '1' && $force eq 'true') { $field->set_indicator(2,'0'); }
4722         $field->add_subfields( '9' => $new_9_to_set );
4723     }
4724 }
4725
4726 return $marc_xml->as_xml_record();
4727
4728 $function$;
4729
4730 DROP FUNCTION IF EXISTS migration_tools.owner_change_sf9_substring_match(TEXT,TEXT,TEXT,TEXT);
4731 CREATE OR REPLACE FUNCTION migration_tools.owner_change_sf9_substring_match (marc_xml TEXT, substring_old_value TEXT, new_value TEXT, fix_indicators TEXT)
4732  RETURNS TEXT
4733  LANGUAGE plperlu
4734 AS $function$
4735 use strict;
4736 use warnings;
4737
4738 use MARC::Record;
4739 use MARC::File::XML (BinaryEncoding => 'utf8');
4740
4741 binmode(STDERR, ':bytes');
4742 binmode(STDOUT, ':utf8');
4743 binmode(STDERR, ':utf8');
4744
4745 my $marc_xml = shift;
4746 my $substring_old_value = shift;
4747 my $new_value = shift;
4748 my $fix_indicators = shift;
4749
4750 $marc_xml =~ s/(<leader>.........)./${1}a/;
4751
4752 eval {
4753     $marc_xml = MARC::Record->new_from_xml($marc_xml);
4754 };
4755 if ($@) {
4756     #elog("could not parse $bibid: $@\n");
4757     import MARC::File::XML (BinaryEncoding => 'utf8');
4758     return $marc_xml;
4759 }
4760
4761 my @uris = $marc_xml->field('856');
4762 return $marc_xml->as_xml_record() unless @uris;
4763
4764 foreach my $field (@uris) {
4765     my $ind1 = $field->indicator('1');
4766     if (defined $ind1) {
4767             if ($ind1 ne '1' && $ind1 ne '4' && $fix_indicators eq 'true') {
4768             $field->set_indicator(1,'4');
4769         }
4770     }
4771     my $ind2 = $field->indicator('2');
4772     if (defined $ind2) {
4773         if ($ind2 ne '0' && $ind2 ne '1' && $fix_indicators eq 'true') {
4774             $field->set_indicator(2,'0');
4775         }
4776     }
4777     if ($field->as_string('9') =~ qr/$substring_old_value/) {
4778         $field->delete_subfield('9');
4779         $field->add_subfields( '9' => $new_value );
4780     }
4781     $marc_xml->delete_field($field); # -- we're going to dedup and add them back
4782 }
4783
4784 my %hash = (map { ($_->as_usmarc => $_) } @uris); # -- courtesy of an old Mike Rylander post :-)
4785 $marc_xml->insert_fields_ordered( values( %hash ) );
4786
4787 return $marc_xml->as_xml_record();
4788
4789 $function$;
4790
4791 DROP FUNCTION IF EXISTS migration_tools.owner_change_sf9_substring_match2(TEXT,TEXT,TEXT,TEXT,TEXT);
4792 CREATE OR REPLACE FUNCTION migration_tools.owner_change_sf9_substring_match2 (marc_xml TEXT, qualifying_match TEXT, substring_old_value TEXT, new_value TEXT, fix_indicators TEXT)
4793  RETURNS TEXT
4794  LANGUAGE plperlu
4795 AS $function$
4796 use strict;
4797 use warnings;
4798
4799 use MARC::Record;
4800 use MARC::File::XML (BinaryEncoding => 'utf8');
4801
4802 binmode(STDERR, ':bytes');
4803 binmode(STDOUT, ':utf8');
4804 binmode(STDERR, ':utf8');
4805
4806 my $marc_xml = shift;
4807 my $qualifying_match = shift;
4808 my $substring_old_value = shift;
4809 my $new_value = shift;
4810 my $fix_indicators = shift;
4811
4812 $marc_xml =~ s/(<leader>.........)./${1}a/;
4813
4814 eval {
4815     $marc_xml = MARC::Record->new_from_xml($marc_xml);
4816 };
4817 if ($@) {
4818     #elog("could not parse $bibid: $@\n");
4819     import MARC::File::XML (BinaryEncoding => 'utf8');
4820     return $marc_xml;
4821 }
4822
4823 my @unqualified_uris = $marc_xml->field('856');
4824 my @uris = ();
4825 foreach my $field (@unqualified_uris) {
4826     if ($field->as_string() =~ qr/$qualifying_match/) {
4827         push @uris, $field;
4828     }
4829 }
4830 return $marc_xml->as_xml_record() unless @uris;
4831
4832 foreach my $field (@uris) {
4833     my $ind1 = $field->indicator('1');
4834     if (defined $ind1) {
4835             if ($ind1 ne '1' && $ind1 ne '4' && $fix_indicators eq 'true') {
4836             $field->set_indicator(1,'4');
4837         }
4838     }
4839     my $ind2 = $field->indicator('2');
4840     if (defined $ind2) {
4841         if ($ind2 ne '0' && $ind2 ne '1' && $fix_indicators eq 'true') {
4842             $field->set_indicator(2,'0');
4843         }
4844     }
4845     if ($field->as_string('9') =~ qr/$substring_old_value/) {
4846         $field->delete_subfield('9');
4847         $field->add_subfields( '9' => $new_value );
4848     }
4849     $marc_xml->delete_field($field); # -- we're going to dedup and add them back
4850 }
4851
4852 my %hash = (map { ($_->as_usmarc => $_) } @uris); # -- courtesy of an old Mike Rylander post :-)
4853 $marc_xml->insert_fields_ordered( values( %hash ) );
4854
4855 return $marc_xml->as_xml_record();
4856
4857 $function$;
4858
4859 -- strip marc tag
4860 DROP FUNCTION IF EXISTS migration_tools.strip_tag(TEXT,TEXT);
4861 CREATE OR REPLACE FUNCTION migration_tools.strip_tag(marc TEXT, tag TEXT)
4862  RETURNS TEXT
4863  LANGUAGE plperlu
4864 AS $function$
4865 use strict;
4866 use warnings;
4867
4868 use MARC::Record;
4869 use MARC::File::XML (BinaryEncoding => 'utf8');
4870
4871 binmode(STDERR, ':bytes');
4872 binmode(STDOUT, ':utf8');
4873 binmode(STDERR, ':utf8');
4874
4875 my $marc_xml = shift;
4876 my $tag = shift;
4877
4878 $marc_xml =~ s/(<leader>.........)./${1}a/;
4879
4880 eval {
4881     $marc_xml = MARC::Record->new_from_xml($marc_xml);
4882 };
4883 if ($@) {
4884     #elog("could not parse $bibid: $@\n");
4885     import MARC::File::XML (BinaryEncoding => 'utf8');
4886     return $marc_xml;
4887 }
4888
4889 my @fields = $marc_xml->field($tag);
4890 return $marc_xml->as_xml_record() unless @fields;
4891
4892 $marc_xml->delete_fields(@fields);
4893
4894 return $marc_xml->as_xml_record();
4895
4896 $function$;
4897
4898 -- consolidate marc tag
4899 DROP FUNCTION IF EXISTS migration_tools.consolidate_tag(TEXT,TEXT);
4900 CREATE OR REPLACE FUNCTION migration_tools.consolidate_tag(marc TEXT, tag TEXT)
4901  RETURNS TEXT
4902  LANGUAGE plperlu
4903 AS $function$
4904 use strict;
4905 use warnings;
4906
4907 use MARC::Record;
4908 use MARC::File::XML (BinaryEncoding => 'utf8');
4909
4910 binmode(STDERR, ':bytes');
4911 binmode(STDOUT, ':utf8');
4912 binmode(STDERR, ':utf8');
4913
4914 my $marc_xml = shift;
4915 my $tag = shift;
4916
4917 $marc_xml =~ s/(<leader>.........)./${1}a/;
4918
4919 eval {
4920     $marc_xml = MARC::Record->new_from_xml($marc_xml);
4921 };
4922 if ($@) {
4923     #elog("could not parse $bibid: $@\n");
4924     import MARC::File::XML (BinaryEncoding => 'utf8');
4925     return $marc_xml;
4926 }
4927
4928 my @fields = $marc_xml->field($tag);
4929 return $marc_xml->as_xml_record() unless @fields;
4930
4931 my @combined_subfield_refs = ();
4932 my @combined_subfields = ();
4933 foreach my $field (@fields) {
4934     my @subfield_refs = $field->subfields();
4935     push @combined_subfield_refs, @subfield_refs;
4936 }
4937
4938 my @sorted_subfield_refs = reverse sort { $a->[0] <=> $b->[0] } @combined_subfield_refs;
4939
4940 while ( my $tuple = pop( @sorted_subfield_refs ) ) {
4941     my ($code,$data) = @$tuple;
4942     unshift( @combined_subfields, $code, $data );
4943 }
4944
4945 $marc_xml->delete_fields(@fields);
4946
4947 my $new_field = new MARC::Field(
4948     $tag,
4949     $fields[0]->indicator(1),
4950     $fields[0]->indicator(2),
4951     @combined_subfields
4952 );
4953
4954 $marc_xml->insert_grouped_field( $new_field );
4955
4956 return $marc_xml->as_xml_record();
4957
4958 $function$;
4959
4960 -- convenience function for linking to the item staging table
4961
4962 CREATE OR REPLACE FUNCTION migration_tools.handle_item_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
4963     DECLARE
4964         table_schema ALIAS FOR $1;
4965         table_name ALIAS FOR $2;
4966         foreign_column_name ALIAS FOR $3;
4967         main_column_name ALIAS FOR $4;
4968         btrim_desired ALIAS FOR $5;
4969         proceed BOOLEAN;
4970     BEGIN
4971         EXECUTE 'SELECT EXISTS (
4972             SELECT 1
4973             FROM information_schema.columns
4974             WHERE table_schema = $1
4975             AND table_name = $2
4976             and column_name = $3
4977         )' INTO proceed USING table_schema, table_name, foreign_column_name;
4978         IF NOT proceed THEN
4979             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
4980         END IF;
4981
4982         EXECUTE 'SELECT EXISTS (
4983             SELECT 1
4984             FROM information_schema.columns
4985             WHERE table_schema = $1
4986             AND table_name = ''asset_copy_legacy''
4987             and column_name = $2
4988         )' INTO proceed USING table_schema, main_column_name;
4989         IF NOT proceed THEN
4990             RAISE EXCEPTION 'No %.asset_copy_legacy with column %', table_schema, main_column_name; 
4991         END IF;
4992
4993         EXECUTE 'ALTER TABLE '
4994             || quote_ident(table_name)
4995             || ' DROP COLUMN IF EXISTS x_item';
4996         EXECUTE 'ALTER TABLE '
4997             || quote_ident(table_name)
4998             || ' ADD COLUMN x_item BIGINT';
4999
5000         IF btrim_desired THEN
5001             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
5002                 || ' SET x_item = b.id FROM asset_copy_legacy b'
5003                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
5004                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
5005         ELSE
5006             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
5007                 || ' SET x_item = b.id FROM asset_copy_legacy b'
5008                 || ' WHERE a.' || quote_ident(foreign_column_name)
5009                 || ' = b.' || quote_ident(main_column_name);
5010         END IF;
5011
5012         --EXECUTE 'SELECT migration_tools.assert(
5013         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_item IS NULL),
5014         --    ''Cannot link every barcode'',
5015         --    ''Every barcode linked''
5016         --);';
5017
5018     END;
5019 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5020
5021 -- convenience function for linking to the user staging table
5022
5023 CREATE OR REPLACE FUNCTION migration_tools.handle_user_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
5024     DECLARE
5025         table_schema ALIAS FOR $1;
5026         table_name ALIAS FOR $2;
5027         foreign_column_name ALIAS FOR $3;
5028         main_column_name ALIAS FOR $4;
5029         btrim_desired ALIAS FOR $5;
5030         proceed BOOLEAN;
5031     BEGIN
5032         EXECUTE 'SELECT EXISTS (
5033             SELECT 1
5034             FROM information_schema.columns
5035             WHERE table_schema = $1
5036             AND table_name = $2
5037             and column_name = $3
5038         )' INTO proceed USING table_schema, table_name, foreign_column_name;
5039         IF NOT proceed THEN
5040             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
5041         END IF;
5042
5043         EXECUTE 'SELECT EXISTS (
5044             SELECT 1
5045             FROM information_schema.columns
5046             WHERE table_schema = $1
5047             AND table_name = ''actor_usr_legacy''
5048             and column_name = $2
5049         )' INTO proceed USING table_schema, main_column_name;
5050         IF NOT proceed THEN
5051             RAISE EXCEPTION 'No %.actor_usr_legacy with column %', table_schema, main_column_name; 
5052         END IF;
5053
5054         EXECUTE 'ALTER TABLE '
5055             || quote_ident(table_name)
5056             || ' DROP COLUMN IF EXISTS x_user';
5057         EXECUTE 'ALTER TABLE '
5058             || quote_ident(table_name)
5059             || ' ADD COLUMN x_user INTEGER';
5060
5061         IF btrim_desired THEN
5062             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
5063                 || ' SET x_user = b.id FROM actor_usr_legacy b'
5064                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
5065                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
5066         ELSE
5067             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
5068                 || ' SET x_user = b.id FROM actor_usr_legacy b'
5069                 || ' WHERE a.' || quote_ident(foreign_column_name)
5070                 || ' = b.' || quote_ident(main_column_name);
5071         END IF;
5072
5073         --EXECUTE 'SELECT migration_tools.assert(
5074         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_user IS NULL),
5075         --    ''Cannot link every barcode'',
5076         --    ''Every barcode linked''
5077         --);';
5078
5079     END;
5080 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5081
5082 -- convenience function for linking two tables
5083 -- e.g. select migration_tools.handle_link(:'migschema','asset_copy','barcode','test_foo','l_barcode','x_acp_id',false);
5084 CREATE OR REPLACE FUNCTION migration_tools.handle_link (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
5085     DECLARE
5086         table_schema ALIAS FOR $1;
5087         table_a ALIAS FOR $2;
5088         column_a ALIAS FOR $3;
5089         table_b ALIAS FOR $4;
5090         column_b ALIAS FOR $5;
5091         column_x ALIAS FOR $6;
5092         btrim_desired ALIAS FOR $7;
5093         proceed BOOLEAN;
5094     BEGIN
5095         EXECUTE 'SELECT EXISTS (
5096             SELECT 1
5097             FROM information_schema.columns
5098             WHERE table_schema = $1
5099             AND table_name = $2
5100             and column_name = $3
5101         )' INTO proceed USING table_schema, table_a, column_a;
5102         IF NOT proceed THEN
5103             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5104         END IF;
5105
5106         EXECUTE 'SELECT EXISTS (
5107             SELECT 1
5108             FROM information_schema.columns
5109             WHERE table_schema = $1
5110             AND table_name = $2
5111             and column_name = $3
5112         )' INTO proceed USING table_schema, table_b, column_b;
5113         IF NOT proceed THEN
5114             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5115         END IF;
5116
5117         EXECUTE 'ALTER TABLE '
5118             || quote_ident(table_b)
5119             || ' DROP COLUMN IF EXISTS ' || quote_ident(column_x);
5120         EXECUTE 'ALTER TABLE '
5121             || quote_ident(table_b)
5122             || ' ADD COLUMN ' || quote_ident(column_x) || ' BIGINT';
5123
5124         IF btrim_desired THEN
5125             EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5126                 || ' SET ' || quote_ident(column_x) || ' = a.id FROM ' || quote_ident(table_a) || ' a'
5127                 || ' WHERE BTRIM(a.' || quote_ident(column_a)
5128                 || ') = BTRIM(b.' || quote_ident(column_b) || ')';
5129         ELSE
5130             EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5131                 || ' SET ' || quote_ident(column_x) || ' = a.id FROM ' || quote_ident(table_a) || ' a'
5132                 || ' WHERE a.' || quote_ident(column_a)
5133                 || ' = b.' || quote_ident(column_b);
5134         END IF;
5135
5136     END;
5137 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5138
5139 -- convenience function for linking two tables, but copying column w into column x instead of "id"
5140 -- e.g. select migration_tools.handle_link2(:'migschema','asset_copy','barcode','test_foo','l_barcode','id','x_acp_id',false);
5141 CREATE OR REPLACE FUNCTION migration_tools.handle_link2 (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
5142     DECLARE
5143         table_schema ALIAS FOR $1;
5144         table_a ALIAS FOR $2;
5145         column_a ALIAS FOR $3;
5146         table_b ALIAS FOR $4;
5147         column_b ALIAS FOR $5;
5148         column_w ALIAS FOR $6;
5149         column_x ALIAS FOR $7;
5150         btrim_desired ALIAS FOR $8;
5151         proceed BOOLEAN;
5152     BEGIN
5153         EXECUTE 'SELECT EXISTS (
5154             SELECT 1
5155             FROM information_schema.columns
5156             WHERE table_schema = $1
5157             AND table_name = $2
5158             and column_name = $3
5159         )' INTO proceed USING table_schema, table_a, column_a;
5160         IF NOT proceed THEN
5161             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5162         END IF;
5163
5164         EXECUTE 'SELECT EXISTS (
5165             SELECT 1
5166             FROM information_schema.columns
5167             WHERE table_schema = $1
5168             AND table_name = $2
5169             and column_name = $3
5170         )' INTO proceed USING table_schema, table_b, column_b;
5171         IF NOT proceed THEN
5172             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5173         END IF;
5174
5175         EXECUTE 'ALTER TABLE '
5176             || quote_ident(table_b)
5177             || ' DROP COLUMN IF EXISTS ' || quote_ident(column_x);
5178         EXECUTE 'ALTER TABLE '
5179             || quote_ident(table_b)
5180             || ' ADD COLUMN ' || quote_ident(column_x) || ' TEXT';
5181
5182         IF btrim_desired THEN
5183             EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5184                 || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5185                 || ' WHERE BTRIM(a.' || quote_ident(column_a)
5186                 || ') = BTRIM(b.' || quote_ident(column_b) || ')';
5187         ELSE
5188             EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5189                 || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5190                 || ' WHERE a.' || quote_ident(column_a)
5191                 || ' = b.' || quote_ident(column_b);
5192         END IF;
5193
5194     END;
5195 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5196
5197 -- 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
5198 -- e.g. select migration_tools.handle_link3(:'migschema','asset_copy','barcode','test_foo','l_barcode','id','x_acp_id');
5199 CREATE OR REPLACE FUNCTION migration_tools.handle_link3 (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5200     DECLARE
5201         table_schema ALIAS FOR $1;
5202         table_a ALIAS FOR $2;
5203         column_a ALIAS FOR $3;
5204         table_b ALIAS FOR $4;
5205         column_b ALIAS FOR $5;
5206         column_w ALIAS FOR $6;
5207         column_x ALIAS FOR $7;
5208         proceed BOOLEAN;
5209     BEGIN
5210         EXECUTE 'SELECT EXISTS (
5211             SELECT 1
5212             FROM information_schema.columns
5213             WHERE table_schema = $1
5214             AND table_name = $2
5215             and column_name = $3
5216         )' INTO proceed USING table_schema, table_a, column_a;
5217         IF NOT proceed THEN
5218             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5219         END IF;
5220
5221         EXECUTE 'SELECT EXISTS (
5222             SELECT 1
5223             FROM information_schema.columns
5224             WHERE table_schema = $1
5225             AND table_name = $2
5226             and column_name = $3
5227         )' INTO proceed USING table_schema, table_b, column_b;
5228         IF NOT proceed THEN
5229             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5230         END IF;
5231
5232         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5233             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5234             || ' WHERE a.' || quote_ident(column_a)
5235             || ' = b.' || quote_ident(column_b);
5236
5237     END;
5238 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5239
5240 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_skip_null_or_empty_string (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5241     DECLARE
5242         table_schema ALIAS FOR $1;
5243         table_a ALIAS FOR $2;
5244         column_a ALIAS FOR $3;
5245         table_b ALIAS FOR $4;
5246         column_b ALIAS FOR $5;
5247         column_w ALIAS FOR $6;
5248         column_x ALIAS FOR $7;
5249         proceed BOOLEAN;
5250     BEGIN
5251         EXECUTE 'SELECT EXISTS (
5252             SELECT 1
5253             FROM information_schema.columns
5254             WHERE table_schema = $1
5255             AND table_name = $2
5256             and column_name = $3
5257         )' INTO proceed USING table_schema, table_a, column_a;
5258         IF NOT proceed THEN
5259             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5260         END IF;
5261
5262         EXECUTE 'SELECT EXISTS (
5263             SELECT 1
5264             FROM information_schema.columns
5265             WHERE table_schema = $1
5266             AND table_name = $2
5267             and column_name = $3
5268         )' INTO proceed USING table_schema, table_b, column_b;
5269         IF NOT proceed THEN
5270             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5271         END IF;
5272
5273         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5274             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5275             || ' WHERE a.' || quote_ident(column_a)
5276             || ' = b.' || quote_ident(column_b)
5277             || ' AND NULLIF(a.' || quote_ident(column_w) || ','''') IS NOT NULL';
5278
5279     END;
5280 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5281
5282 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_skip_null (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5283     DECLARE
5284         table_schema ALIAS FOR $1;
5285         table_a ALIAS FOR $2;
5286         column_a ALIAS FOR $3;
5287         table_b ALIAS FOR $4;
5288         column_b ALIAS FOR $5;
5289         column_w ALIAS FOR $6;
5290         column_x ALIAS FOR $7;
5291         proceed BOOLEAN;
5292     BEGIN
5293         EXECUTE 'SELECT EXISTS (
5294             SELECT 1
5295             FROM information_schema.columns
5296             WHERE table_schema = $1
5297             AND table_name = $2
5298             and column_name = $3
5299         )' INTO proceed USING table_schema, table_a, column_a;
5300         IF NOT proceed THEN
5301             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5302         END IF;
5303
5304         EXECUTE 'SELECT EXISTS (
5305             SELECT 1
5306             FROM information_schema.columns
5307             WHERE table_schema = $1
5308             AND table_name = $2
5309             and column_name = $3
5310         )' INTO proceed USING table_schema, table_b, column_b;
5311         IF NOT proceed THEN
5312             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5313         END IF;
5314
5315         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5316             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5317             || ' WHERE a.' || quote_ident(column_a)
5318             || ' = b.' || quote_ident(column_b)
5319             || ' AND a.' || quote_ident(column_w) || ' IS NOT NULL';
5320
5321     END;
5322 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5323
5324 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_skip_true (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5325     DECLARE
5326         table_schema ALIAS FOR $1;
5327         table_a ALIAS FOR $2;
5328         column_a ALIAS FOR $3;
5329         table_b ALIAS FOR $4;
5330         column_b ALIAS FOR $5;
5331         column_w ALIAS FOR $6;
5332         column_x ALIAS FOR $7;
5333         proceed BOOLEAN;
5334     BEGIN
5335         EXECUTE 'SELECT EXISTS (
5336             SELECT 1
5337             FROM information_schema.columns
5338             WHERE table_schema = $1
5339             AND table_name = $2
5340             and column_name = $3
5341         )' INTO proceed USING table_schema, table_a, column_a;
5342         IF NOT proceed THEN
5343             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5344         END IF;
5345
5346         EXECUTE 'SELECT EXISTS (
5347             SELECT 1
5348             FROM information_schema.columns
5349             WHERE table_schema = $1
5350             AND table_name = $2
5351             and column_name = $3
5352         )' INTO proceed USING table_schema, table_b, column_b;
5353         IF NOT proceed THEN
5354             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5355         END IF;
5356
5357         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5358             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5359             || ' WHERE a.' || quote_ident(column_a)
5360             || ' = b.' || quote_ident(column_b)
5361             || ' AND a.' || quote_ident(column_w) || ' IS NOT TRUE';
5362
5363     END;
5364 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5365
5366 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_skip_false (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5367     DECLARE
5368         table_schema ALIAS FOR $1;
5369         table_a ALIAS FOR $2;
5370         column_a ALIAS FOR $3;
5371         table_b ALIAS FOR $4;
5372         column_b ALIAS FOR $5;
5373         column_w ALIAS FOR $6;
5374         column_x ALIAS FOR $7;
5375         proceed BOOLEAN;
5376     BEGIN
5377         EXECUTE 'SELECT EXISTS (
5378             SELECT 1
5379             FROM information_schema.columns
5380             WHERE table_schema = $1
5381             AND table_name = $2
5382             and column_name = $3
5383         )' INTO proceed USING table_schema, table_a, column_a;
5384         IF NOT proceed THEN
5385             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5386         END IF;
5387
5388         EXECUTE 'SELECT EXISTS (
5389             SELECT 1
5390             FROM information_schema.columns
5391             WHERE table_schema = $1
5392             AND table_name = $2
5393             and column_name = $3
5394         )' INTO proceed USING table_schema, table_b, column_b;
5395         IF NOT proceed THEN
5396             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5397         END IF;
5398
5399         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5400             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5401             || ' WHERE a.' || quote_ident(column_a)
5402             || ' = b.' || quote_ident(column_b)
5403             || ' AND a.' || quote_ident(column_w) || ' IS NOT FALSE';
5404
5405     END;
5406 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5407
5408 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_concat_skip_null (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5409     DECLARE
5410         table_schema ALIAS FOR $1;
5411         table_a ALIAS FOR $2;
5412         column_a ALIAS FOR $3;
5413         table_b ALIAS FOR $4;
5414         column_b ALIAS FOR $5;
5415         column_w ALIAS FOR $6;
5416         column_x ALIAS FOR $7;
5417         proceed BOOLEAN;
5418     BEGIN
5419         EXECUTE 'SELECT EXISTS (
5420             SELECT 1
5421             FROM information_schema.columns
5422             WHERE table_schema = $1
5423             AND table_name = $2
5424             and column_name = $3
5425         )' INTO proceed USING table_schema, table_a, column_a;
5426         IF NOT proceed THEN
5427             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5428         END IF;
5429
5430         EXECUTE 'SELECT EXISTS (
5431             SELECT 1
5432             FROM information_schema.columns
5433             WHERE table_schema = $1
5434             AND table_name = $2
5435             and column_name = $3
5436         )' INTO proceed USING table_schema, table_b, column_b;
5437         IF NOT proceed THEN
5438             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5439         END IF;
5440
5441         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5442             || ' SET ' || quote_ident(column_x) || ' = CONCAT_WS('' ; '',b.' || quote_ident(column_x) || ',a.' || quote_ident(column_w) || ') FROM ' || quote_ident(table_a) || ' a'
5443             || ' WHERE a.' || quote_ident(column_a)
5444             || ' = b.' || quote_ident(column_b)
5445             || ' AND NULLIF(a.' || quote_ident(column_w) || ','''') IS NOT NULL';
5446
5447     END;
5448 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5449
5450 -- convenience function for handling desired asset stat cats
5451
5452 CREATE OR REPLACE FUNCTION migration_tools.vivicate_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5453     DECLARE
5454         table_schema ALIAS FOR $1;
5455         table_name ALIAS FOR $2;
5456         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
5457         org_shortname ALIAS FOR $4;
5458         proceed BOOLEAN;
5459         org INTEGER;
5460         org_list INTEGER[];
5461         sc TEXT;
5462         sce TEXT;
5463     BEGIN
5464
5465         SELECT 'desired_sc' || field_suffix INTO sc;
5466         SELECT 'desired_sce' || field_suffix INTO sce;
5467
5468         EXECUTE 'SELECT EXISTS (
5469             SELECT 1
5470             FROM information_schema.columns
5471             WHERE table_schema = $1
5472             AND table_name = $2
5473             and column_name = $3
5474         )' INTO proceed USING table_schema, table_name, sc;
5475         IF NOT proceed THEN
5476             RAISE EXCEPTION 'Missing column %', sc; 
5477         END IF;
5478         EXECUTE 'SELECT EXISTS (
5479             SELECT 1
5480             FROM information_schema.columns
5481             WHERE table_schema = $1
5482             AND table_name = $2
5483             and column_name = $3
5484         )' INTO proceed USING table_schema, table_name, sce;
5485         IF NOT proceed THEN
5486             RAISE EXCEPTION 'Missing column %', sce; 
5487         END IF;
5488
5489         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
5490         IF org IS NULL THEN
5491             RAISE EXCEPTION 'Cannot find org by shortname';
5492         END IF;
5493         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
5494
5495         -- caller responsible for their own truncates though we try to prevent duplicates
5496         EXECUTE 'INSERT INTO asset_stat_cat (owner, name)
5497             SELECT DISTINCT
5498                  $1
5499                 ,BTRIM('||sc||')
5500             FROM 
5501                 ' || quote_ident(table_name) || '
5502             WHERE
5503                 NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
5504                 AND NOT EXISTS (
5505                     SELECT id
5506                     FROM asset.stat_cat
5507                     WHERE owner = ANY ($2)
5508                     AND name = BTRIM('||sc||')
5509                 )
5510                 AND NOT EXISTS (
5511                     SELECT id
5512                     FROM asset_stat_cat
5513                     WHERE owner = ANY ($2)
5514                     AND name = BTRIM('||sc||')
5515                 )
5516             ORDER BY 2;'
5517         USING org, org_list;
5518
5519         EXECUTE 'INSERT INTO asset_stat_cat_entry (stat_cat, owner, value)
5520             SELECT DISTINCT
5521                 COALESCE(
5522                     (SELECT id
5523                         FROM asset.stat_cat
5524                         WHERE owner = ANY ($2)
5525                         AND BTRIM('||sc||') = BTRIM(name))
5526                    ,(SELECT id
5527                         FROM asset_stat_cat
5528                         WHERE owner = ANY ($2)
5529                         AND BTRIM('||sc||') = BTRIM(name))
5530                 )
5531                 ,$1
5532                 ,BTRIM('||sce||')
5533             FROM 
5534                 ' || quote_ident(table_name) || '
5535             WHERE
5536                     NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
5537                 AND NULLIF(BTRIM('||sce||'),'''') IS NOT NULL
5538                 AND NOT EXISTS (
5539                     SELECT id
5540                     FROM asset.stat_cat_entry
5541                     WHERE stat_cat = (
5542                         SELECT id
5543                         FROM asset.stat_cat
5544                         WHERE owner = ANY ($2)
5545                         AND BTRIM('||sc||') = BTRIM(name)
5546                     ) AND value = BTRIM('||sce||')
5547                     AND owner = ANY ($2)
5548                 )
5549                 AND NOT EXISTS (
5550                     SELECT id
5551                     FROM asset_stat_cat_entry
5552                     WHERE stat_cat = (
5553                         SELECT id
5554                         FROM asset_stat_cat
5555                         WHERE owner = ANY ($2)
5556                         AND BTRIM('||sc||') = BTRIM(name)
5557                     ) AND value = BTRIM('||sce||')
5558                     AND owner = ANY ($2)
5559                 )
5560             ORDER BY 1,3;'
5561         USING org, org_list;
5562     END;
5563 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5564
5565 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5566     DECLARE
5567         table_schema ALIAS FOR $1;
5568         table_name ALIAS FOR $2;
5569         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
5570         org_shortname ALIAS FOR $4;
5571         proceed BOOLEAN;
5572         org INTEGER;
5573         org_list INTEGER[];
5574         o INTEGER;
5575         sc TEXT;
5576         sce TEXT;
5577     BEGIN
5578         SELECT 'desired_sc' || field_suffix INTO sc;
5579         SELECT 'desired_sce' || field_suffix INTO sce;
5580         EXECUTE 'SELECT EXISTS (
5581             SELECT 1
5582             FROM information_schema.columns
5583             WHERE table_schema = $1
5584             AND table_name = $2
5585             and column_name = $3
5586         )' INTO proceed USING table_schema, table_name, sc;
5587         IF NOT proceed THEN
5588             RAISE EXCEPTION 'Missing column %', sc; 
5589         END IF;
5590         EXECUTE 'SELECT EXISTS (
5591             SELECT 1
5592             FROM information_schema.columns
5593             WHERE table_schema = $1
5594             AND table_name = $2
5595             and column_name = $3
5596         )' INTO proceed USING table_schema, table_name, sce;
5597         IF NOT proceed THEN
5598             RAISE EXCEPTION 'Missing column %', sce; 
5599         END IF;
5600
5601         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
5602         IF org IS NULL THEN
5603             RAISE EXCEPTION 'Cannot find org by shortname';
5604         END IF;
5605
5606         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
5607
5608         EXECUTE 'ALTER TABLE '
5609             || quote_ident(table_name)
5610             || ' DROP COLUMN IF EXISTS x_sc' || field_suffix;
5611         EXECUTE 'ALTER TABLE '
5612             || quote_ident(table_name)
5613             || ' ADD COLUMN x_sc' || field_suffix || ' INTEGER';
5614         EXECUTE 'ALTER TABLE '
5615             || quote_ident(table_name)
5616             || ' DROP COLUMN IF EXISTS x_sce' || field_suffix;
5617         EXECUTE 'ALTER TABLE '
5618             || quote_ident(table_name)
5619             || ' ADD COLUMN x_sce' || field_suffix || ' INTEGER';
5620
5621
5622         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
5623             SET
5624                 x_sc' || field_suffix || ' = id
5625             FROM
5626                 (SELECT id, name, owner FROM asset_stat_cat
5627                     UNION SELECT id, name, owner FROM asset.stat_cat) u
5628             WHERE
5629                     BTRIM(UPPER(u.name)) = BTRIM(UPPER(' || sc || '))
5630                 AND u.owner = ANY ($1);'
5631         USING org_list;
5632
5633         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
5634             SET
5635                 x_sce' || field_suffix || ' = id
5636             FROM
5637                 (SELECT id, stat_cat, owner, value FROM asset_stat_cat_entry
5638                     UNION SELECT id, stat_cat, owner, value FROM asset.stat_cat_entry) u
5639             WHERE
5640                     u.stat_cat = x_sc' || field_suffix || '
5641                 AND BTRIM(UPPER(u.value)) = BTRIM(UPPER(' || sce || '))
5642                 AND u.owner = ANY ($1);'
5643         USING org_list;
5644
5645         EXECUTE 'SELECT migration_tools.assert(
5646             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sc' || field_suffix || ' <> '''' AND x_sc' || field_suffix || ' IS NULL),
5647             ''Cannot find a desired stat cat'',
5648             ''Found all desired stat cats''
5649         );';
5650
5651         EXECUTE 'SELECT migration_tools.assert(
5652             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sce' || field_suffix || ' <> '''' AND x_sce' || field_suffix || ' IS NULL),
5653             ''Cannot find a desired stat cat entry'',
5654             ''Found all desired stat cat entries''
5655         );';
5656
5657     END;
5658 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5659
5660 DROP FUNCTION IF EXISTS migration_tools.btrim_lcolumns(TEXT,TEXT);
5661 CREATE OR REPLACE FUNCTION migration_tools.btrim_lcolumns(s_name TEXT, t_name TEXT) RETURNS BOOLEAN
5662  LANGUAGE plpgsql
5663 AS $function$
5664 DECLARE
5665     c_name     TEXT;
5666 BEGIN
5667
5668     FOR c_name IN SELECT column_name FROM information_schema.columns WHERE 
5669             table_name = t_name
5670             AND table_schema = s_name
5671             AND (data_type='text' OR data_type='character varying')
5672             AND column_name like 'l_%'
5673     LOOP
5674        EXECUTE FORMAT('UPDATE ' || s_name || '.' || t_name || ' SET ' || c_name || ' = BTRIM(' || c_name || ')'); 
5675     END LOOP;  
5676
5677     RETURN TRUE;
5678 END
5679 $function$;
5680
5681 DROP FUNCTION IF EXISTS migration_tools.btrim_columns(TEXT,TEXT);
5682 CREATE OR REPLACE FUNCTION migration_tools.btrim_columns(s_name TEXT, t_name TEXT) RETURNS BOOLEAN
5683  LANGUAGE plpgsql
5684 AS $function$
5685 DECLARE
5686     c_name     TEXT;
5687 BEGIN
5688
5689     FOR c_name IN SELECT column_name FROM information_schema.columns WHERE 
5690             table_name = t_name
5691             AND table_schema = s_name
5692             AND (data_type='text' OR data_type='character varying')
5693     LOOP
5694        EXECUTE FORMAT('UPDATE ' || s_name || '.' || t_name || ' SET ' || c_name || ' = BTRIM(' || c_name || ')'); 
5695     END LOOP;  
5696
5697     RETURN TRUE;
5698 END
5699 $function$;
5700
5701 DROP FUNCTION IF EXISTS migration_tools.null_empty_lcolumns(TEXT,TEXT);
5702 CREATE OR REPLACE FUNCTION migration_tools.null_empty_lcolumns(s_name TEXT, t_name TEXT) RETURNS BOOLEAN
5703  LANGUAGE plpgsql
5704 AS $function$
5705 DECLARE
5706     c_name     TEXT;
5707 BEGIN
5708
5709     FOR c_name IN SELECT column_name FROM information_schema.columns WHERE 
5710             table_name = t_name
5711             AND table_schema = s_name
5712             AND (data_type='text' OR data_type='character varying')
5713             AND column_name like 'l_%'
5714     LOOP
5715        EXECUTE FORMAT('UPDATE ' || s_name || '.' || t_name || ' SET ' || c_name || ' = NULL WHERE ' || c_name || ' = '''' '); 
5716     END LOOP;  
5717
5718     RETURN TRUE;
5719 END
5720 $function$;
5721
5722 DROP FUNCTION IF EXISTS migration_tools.null_empty_columns(TEXT,TEXT);
5723 CREATE OR REPLACE FUNCTION migration_tools.null_empty_columns(s_name TEXT, t_name TEXT) RETURNS BOOLEAN
5724  LANGUAGE plpgsql
5725 AS $function$
5726 DECLARE
5727     c_name     TEXT;
5728 BEGIN
5729
5730     FOR c_name IN SELECT column_name FROM information_schema.columns WHERE
5731             table_name = t_name
5732             AND table_schema = s_name
5733             AND (data_type='text' OR data_type='character varying')
5734     LOOP
5735        EXECUTE FORMAT('UPDATE ' || s_name || '.' || t_name || ' SET ' || c_name || ' = NULL WHERE ' || c_name || ' = '''' ');
5736     END LOOP;
5737
5738     RETURN TRUE;
5739 END
5740 $function$;
5741
5742
5743 -- convenience function for handling item barcode collisions in asset_copy_legacy
5744
5745 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
5746 DECLARE
5747     x_barcode TEXT;
5748     x_id BIGINT;
5749     row_count NUMERIC;
5750     internal_collision_count NUMERIC := 0;
5751     incumbent_collision_count NUMERIC := 0;
5752 BEGIN
5753     FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
5754     LOOP
5755         FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
5756         LOOP
5757             UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
5758             GET DIAGNOSTICS row_count = ROW_COUNT;
5759             internal_collision_count := internal_collision_count + row_count;
5760         END LOOP;
5761     END LOOP;
5762     RAISE INFO '% internal collisions', internal_collision_count;
5763     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
5764     LOOP
5765         FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
5766         LOOP
5767             UPDATE asset_copy_legacy SET barcode = migration_schema || '_incumbent_collision_' || id || '_' || barcode WHERE id = x_id;
5768             GET DIAGNOSTICS row_count = ROW_COUNT;
5769             incumbent_collision_count := incumbent_collision_count + row_count;
5770         END LOOP;
5771     END LOOP;
5772     RAISE INFO '% incumbent collisions', incumbent_collision_count;
5773 END
5774 $function$ LANGUAGE plpgsql;
5775
5776 -- convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
5777 -- this should be ran prior to populating actor_card
5778
5779 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
5780 DECLARE
5781     x_barcode TEXT;
5782     x_id BIGINT;
5783     row_count NUMERIC;
5784     internal_collision_count NUMERIC := 0;
5785     incumbent_barcode_collision_count NUMERIC := 0;
5786     incumbent_usrname_collision_count NUMERIC := 0;
5787 BEGIN
5788     FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
5789     LOOP
5790         FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5791         LOOP
5792             UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
5793             GET DIAGNOSTICS row_count = ROW_COUNT;
5794             internal_collision_count := internal_collision_count + row_count;
5795         END LOOP;
5796     END LOOP;
5797     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
5798
5799     FOR x_barcode IN
5800         SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
5801     LOOP
5802         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5803         LOOP
5804             UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_barcode_collision_' || id || '_' || usrname WHERE id = x_id;
5805             GET DIAGNOSTICS row_count = ROW_COUNT;
5806             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
5807         END LOOP;
5808     END LOOP;
5809     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
5810
5811     FOR x_barcode IN
5812         SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
5813     LOOP
5814         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5815         LOOP
5816             UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_usrname_collision_' || id || '_' || usrname WHERE id = x_id;
5817             GET DIAGNOSTICS row_count = ROW_COUNT;
5818             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
5819         END LOOP;
5820     END LOOP;
5821     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
5822 END
5823 $function$ LANGUAGE plpgsql;
5824
5825 -- alternate version: convenience function for handling item barcode collisions in asset_copy_legacy
5826
5827 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
5828 DECLARE
5829     x_barcode TEXT;
5830     x_id BIGINT;
5831     row_count NUMERIC;
5832     internal_collision_count NUMERIC := 0;
5833     incumbent_collision_count NUMERIC := 0;
5834 BEGIN
5835     FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
5836     LOOP
5837         FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
5838         LOOP
5839             UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
5840             GET DIAGNOSTICS row_count = ROW_COUNT;
5841             internal_collision_count := internal_collision_count + row_count;
5842         END LOOP;
5843     END LOOP;
5844     RAISE INFO '% internal collisions', internal_collision_count;
5845     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
5846     LOOP
5847         FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
5848         LOOP
5849             UPDATE asset_copy_legacy SET barcode = migration_schema || '_' || barcode WHERE id = x_id;
5850             GET DIAGNOSTICS row_count = ROW_COUNT;
5851             incumbent_collision_count := incumbent_collision_count + row_count;
5852         END LOOP;
5853     END LOOP;
5854     RAISE INFO '% incumbent collisions', incumbent_collision_count;
5855 END
5856 $function$ LANGUAGE plpgsql;
5857
5858 -- alternate version: convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
5859 -- this should be ran prior to populating actor_card
5860
5861 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
5862 DECLARE
5863     x_barcode TEXT;
5864     x_id BIGINT;
5865     row_count NUMERIC;
5866     internal_collision_count NUMERIC := 0;
5867     incumbent_barcode_collision_count NUMERIC := 0;
5868     incumbent_usrname_collision_count NUMERIC := 0;
5869 BEGIN
5870     FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
5871     LOOP
5872         FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5873         LOOP
5874             UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
5875             GET DIAGNOSTICS row_count = ROW_COUNT;
5876             internal_collision_count := internal_collision_count + row_count;
5877         END LOOP;
5878     END LOOP;
5879     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
5880
5881     FOR x_barcode IN
5882         SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
5883     LOOP
5884         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5885         LOOP
5886             UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
5887             GET DIAGNOSTICS row_count = ROW_COUNT;
5888             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
5889         END LOOP;
5890     END LOOP;
5891     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
5892
5893     FOR x_barcode IN
5894         SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
5895     LOOP
5896         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5897         LOOP
5898             UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
5899             GET DIAGNOSTICS row_count = ROW_COUNT;
5900             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
5901         END LOOP;
5902     END LOOP;
5903     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
5904 END
5905 $function$ LANGUAGE plpgsql;
5906
5907 CREATE OR REPLACE FUNCTION migration_tools.is_circ_rule_safe_to_delete( test_matchpoint INTEGER ) RETURNS BOOLEAN AS $func$
5908 -- WARNING: Use at your own risk
5909 -- FIXME: not considering marc_type, marc_form, marc_bib_level, marc_vr_format, usr_age_lower_bound, usr_age_upper_bound, item_age
5910 DECLARE
5911     item_object asset.copy%ROWTYPE;
5912     user_object actor.usr%ROWTYPE;
5913     test_rule_object config.circ_matrix_matchpoint%ROWTYPE;
5914     result_rule_object config.circ_matrix_matchpoint%ROWTYPE;
5915     safe_to_delete BOOLEAN := FALSE;
5916     m action.found_circ_matrix_matchpoint;
5917     result_matchpoint INTEGER;
5918     -- ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] )
5919 BEGIN
5920     SELECT INTO test_rule_object * FROM config.circ_matrix_matchpoint WHERE id = test_matchpoint;
5921     RAISE INFO 'testing rule: %', test_rule_object;
5922
5923     INSERT INTO actor.usr (
5924         profile,
5925         usrname,
5926         passwd,
5927         ident_type,
5928         first_given_name,
5929         family_name,
5930         home_ou,
5931         juvenile
5932     ) SELECT
5933         COALESCE(test_rule_object.grp, 2),
5934         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
5935         MD5(NOW()::TEXT),
5936         1,
5937         'Ima',
5938         'Test',
5939         COALESCE(test_rule_object.user_home_ou, test_rule_object.org_unit),
5940         COALESCE(test_rule_object.juvenile_flag, FALSE)
5941     ;
5942     
5943     SELECT INTO user_object * FROM actor.usr WHERE id = currval('actor.usr_id_seq');
5944
5945     INSERT INTO asset.call_number (
5946         creator,
5947         editor,
5948         record,
5949         owning_lib,
5950         label,
5951         label_class
5952     ) SELECT
5953         1,
5954         1,
5955         -1,
5956         COALESCE(test_rule_object.copy_owning_lib,test_rule_object.org_unit),
5957         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
5958         1
5959     ;
5960
5961     INSERT INTO asset.copy (
5962         barcode,
5963         circ_lib,
5964         creator,
5965         call_number,
5966         editor,
5967         location,
5968         loan_duration,
5969         fine_level,
5970         ref,
5971         circ_modifier
5972     ) SELECT
5973         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
5974         COALESCE(test_rule_object.copy_circ_lib,test_rule_object.org_unit),
5975         1,
5976         currval('asset.call_number_id_seq'),
5977         1,
5978         COALESCE(test_rule_object.copy_location,1),
5979         2,
5980         2,
5981         COALESCE(test_rule_object.ref_flag,FALSE),
5982         test_rule_object.circ_modifier
5983     ;
5984
5985     SELECT INTO item_object * FROM asset.copy WHERE id = currval('asset.copy_id_seq');
5986
5987     SELECT INTO m * FROM action.find_circ_matrix_matchpoint(
5988         test_rule_object.org_unit,
5989         item_object,
5990         user_object,
5991         COALESCE(test_rule_object.is_renewal,FALSE)
5992     );
5993     RAISE INFO 'action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
5994         test_rule_object.org_unit,
5995         item_object.id,
5996         user_object.id,
5997         COALESCE(test_rule_object.is_renewal,FALSE),
5998         m.success,
5999         (m.matchpoint).id,
6000         m.buildrows
6001     ;
6002
6003     FOR result_matchpoint IN SELECT UNNEST(m.buildrows)
6004     LOOP
6005         SELECT INTO result_rule_object * FROM config.circ_matrix_matchpoint WHERE id = result_matchpoint;
6006         RAISE INFO 'considering rule: %', result_rule_object;
6007         IF result_rule_object.id = test_rule_object.id THEN
6008             RAISE INFO 'found self';
6009             CONTINUE;
6010         END IF;
6011         IF (result_rule_object.circulate = test_rule_object.circulate
6012             AND result_rule_object.duration_rule = test_rule_object.duration_rule
6013             AND result_rule_object.recurring_fine_rule = test_rule_object.recurring_fine_rule
6014             AND result_rule_object.max_fine_rule = test_rule_object.max_fine_rule
6015             AND (
6016                 (result_rule_object.hard_due_date IS NULL AND test_rule_object.hard_due_date IS NULL)
6017                 OR (result_rule_object.hard_due_date = test_rule_object.hard_due_date)
6018                 OR (result_rule_object.hard_due_date IS NOT NULL AND test_rule_object.hard_due_date IS NULL)
6019             )
6020             AND (
6021                 (result_rule_object.renewals IS NULL AND test_rule_object.renewals IS NULL)
6022                 OR (result_rule_object.renewals = test_rule_object.renewals)
6023                 OR (result_rule_object.renewals IS NOT NULL AND test_rule_object.renewals IS NULL)
6024             )
6025             AND (
6026                 (result_rule_object.grace_period IS NULL AND test_rule_object.grace_period IS NULL)
6027                 OR (result_rule_object.grace_period = test_rule_object.grace_period)
6028                 OR (result_rule_object.grace_period IS NOT NULL AND test_rule_object.grace_period IS NULL)
6029             )
6030             AND NOT EXISTS (
6031                 SELECT limit_set, fallthrough
6032                 FROM config.circ_matrix_limit_set_map
6033                 WHERE active and matchpoint = test_rule_object.id
6034                 EXCEPT
6035                 SELECT limit_set, fallthrough
6036                 FROM config.circ_matrix_limit_set_map
6037                 WHERE active and matchpoint = result_rule_object.id
6038             )
6039         ) THEN
6040             RAISE INFO 'rule has same outcome';
6041             safe_to_delete := TRUE;
6042         ELSE
6043             RAISE INFO 'rule has different outcome, bail now';
6044             RAISE EXCEPTION 'rollback the item and user tables';
6045         END IF;
6046     END LOOP;
6047
6048     RAISE EXCEPTION 'rollback the item and user tables';
6049
6050 EXCEPTION WHEN OTHERS THEN
6051
6052     RAISE INFO 'inside exception block: %, %', SQLSTATE, SQLERRM;
6053     RETURN safe_to_delete;
6054
6055 END;
6056 $func$ LANGUAGE plpgsql;
6057