added version of strip_tag to take string and subfield as arguments
[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 -- removes tags from record based on tag, subfield and evidence
4899 -- example: strip_tag(marc, '500', 'a', 'gift') will remove 500s with 'gift' as a part of the $a
4900 DROP FUNCTION IF EXISTS migration_tools.strip_tag(TEXT,TEXT,TEXT,TEXT);
4901 CREATE OR REPLACE FUNCTION migration_tools.strip_tag(marc TEXT, tag TEXT, subfield TEXT, evidence TEXT)
4902  RETURNS TEXT
4903  LANGUAGE plperlu
4904 AS $function$
4905 use strict;
4906 use warnings;
4907
4908 use MARC::Record;
4909 use MARC::File::XML (BinaryEncoding => 'utf8');
4910
4911 binmode(STDERR, ':bytes');
4912 binmode(STDOUT, ':utf8');
4913 binmode(STDERR, ':utf8');
4914
4915 my $marc_xml = shift;
4916 my $tag = shift;
4917 my $subfield = shift;
4918 my $evidence = shift;
4919
4920 $marc_xml =~ s/(<leader>.........)./${1}a/;
4921
4922 eval {
4923     $marc_xml = MARC::Record->new_from_xml($marc_xml);
4924 };
4925 if ($@) {
4926     #elog("could not parse $bibid: $@\n");
4927     import MARC::File::XML (BinaryEncoding => 'utf8');
4928     return $marc_xml;
4929 }
4930
4931 my @fields = $marc_xml->field($tag);
4932 return $marc_xml->as_xml_record() unless @fields;
4933
4934 my @fields_to_delete;
4935
4936 foreach my $f (@fields) {
4937     my $sf = lc($f->as_string($subfield));
4938     if ($sf =~ m/$evidence/) { push @fields_to_delete, $f; }
4939 }
4940
4941 $marc_xml->delete_fields(@fields_to_delete);
4942
4943 return $marc_xml->as_xml_record();
4944
4945 $function$;
4946
4947
4948 -- consolidate marc tag
4949 DROP FUNCTION IF EXISTS migration_tools.consolidate_tag(TEXT,TEXT);
4950 CREATE OR REPLACE FUNCTION migration_tools.consolidate_tag(marc TEXT, tag TEXT)
4951  RETURNS TEXT
4952  LANGUAGE plperlu
4953 AS $function$
4954 use strict;
4955 use warnings;
4956
4957 use MARC::Record;
4958 use MARC::File::XML (BinaryEncoding => 'utf8');
4959
4960 binmode(STDERR, ':bytes');
4961 binmode(STDOUT, ':utf8');
4962 binmode(STDERR, ':utf8');
4963
4964 my $marc_xml = shift;
4965 my $tag = shift;
4966
4967 $marc_xml =~ s/(<leader>.........)./${1}a/;
4968
4969 eval {
4970     $marc_xml = MARC::Record->new_from_xml($marc_xml);
4971 };
4972 if ($@) {
4973     #elog("could not parse $bibid: $@\n");
4974     import MARC::File::XML (BinaryEncoding => 'utf8');
4975     return $marc_xml;
4976 }
4977
4978 my @fields = $marc_xml->field($tag);
4979 return $marc_xml->as_xml_record() unless @fields;
4980
4981 my @combined_subfield_refs = ();
4982 my @combined_subfields = ();
4983 foreach my $field (@fields) {
4984     my @subfield_refs = $field->subfields();
4985     push @combined_subfield_refs, @subfield_refs;
4986 }
4987
4988 my @sorted_subfield_refs = reverse sort { $a->[0] <=> $b->[0] } @combined_subfield_refs;
4989
4990 while ( my $tuple = pop( @sorted_subfield_refs ) ) {
4991     my ($code,$data) = @$tuple;
4992     unshift( @combined_subfields, $code, $data );
4993 }
4994
4995 $marc_xml->delete_fields(@fields);
4996
4997 my $new_field = new MARC::Field(
4998     $tag,
4999     $fields[0]->indicator(1),
5000     $fields[0]->indicator(2),
5001     @combined_subfields
5002 );
5003
5004 $marc_xml->insert_grouped_field( $new_field );
5005
5006 return $marc_xml->as_xml_record();
5007
5008 $function$;
5009
5010 -- convenience function for linking to the item staging table
5011
5012 CREATE OR REPLACE FUNCTION migration_tools.handle_item_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
5013     DECLARE
5014         table_schema ALIAS FOR $1;
5015         table_name ALIAS FOR $2;
5016         foreign_column_name ALIAS FOR $3;
5017         main_column_name ALIAS FOR $4;
5018         btrim_desired ALIAS FOR $5;
5019         proceed BOOLEAN;
5020     BEGIN
5021         EXECUTE 'SELECT EXISTS (
5022             SELECT 1
5023             FROM information_schema.columns
5024             WHERE table_schema = $1
5025             AND table_name = $2
5026             and column_name = $3
5027         )' INTO proceed USING table_schema, table_name, foreign_column_name;
5028         IF NOT proceed THEN
5029             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
5030         END IF;
5031
5032         EXECUTE 'SELECT EXISTS (
5033             SELECT 1
5034             FROM information_schema.columns
5035             WHERE table_schema = $1
5036             AND table_name = ''asset_copy_legacy''
5037             and column_name = $2
5038         )' INTO proceed USING table_schema, main_column_name;
5039         IF NOT proceed THEN
5040             RAISE EXCEPTION 'No %.asset_copy_legacy with column %', table_schema, main_column_name; 
5041         END IF;
5042
5043         EXECUTE 'ALTER TABLE '
5044             || quote_ident(table_name)
5045             || ' DROP COLUMN IF EXISTS x_item';
5046         EXECUTE 'ALTER TABLE '
5047             || quote_ident(table_name)
5048             || ' ADD COLUMN x_item BIGINT';
5049
5050         IF btrim_desired THEN
5051             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
5052                 || ' SET x_item = b.id FROM asset_copy_legacy b'
5053                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
5054                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
5055         ELSE
5056             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
5057                 || ' SET x_item = b.id FROM asset_copy_legacy b'
5058                 || ' WHERE a.' || quote_ident(foreign_column_name)
5059                 || ' = b.' || quote_ident(main_column_name);
5060         END IF;
5061
5062         --EXECUTE 'SELECT migration_tools.assert(
5063         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_item IS NULL),
5064         --    ''Cannot link every barcode'',
5065         --    ''Every barcode linked''
5066         --);';
5067
5068     END;
5069 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5070
5071 -- convenience function for linking to the user staging table
5072
5073 CREATE OR REPLACE FUNCTION migration_tools.handle_user_barcode (TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
5074     DECLARE
5075         table_schema ALIAS FOR $1;
5076         table_name ALIAS FOR $2;
5077         foreign_column_name ALIAS FOR $3;
5078         main_column_name ALIAS FOR $4;
5079         btrim_desired ALIAS FOR $5;
5080         proceed BOOLEAN;
5081     BEGIN
5082         EXECUTE 'SELECT EXISTS (
5083             SELECT 1
5084             FROM information_schema.columns
5085             WHERE table_schema = $1
5086             AND table_name = $2
5087             and column_name = $3
5088         )' INTO proceed USING table_schema, table_name, foreign_column_name;
5089         IF NOT proceed THEN
5090             RAISE EXCEPTION '%.% missing column %', table_schema, table_name, foreign_column_name; 
5091         END IF;
5092
5093         EXECUTE 'SELECT EXISTS (
5094             SELECT 1
5095             FROM information_schema.columns
5096             WHERE table_schema = $1
5097             AND table_name = ''actor_usr_legacy''
5098             and column_name = $2
5099         )' INTO proceed USING table_schema, main_column_name;
5100         IF NOT proceed THEN
5101             RAISE EXCEPTION 'No %.actor_usr_legacy with column %', table_schema, main_column_name; 
5102         END IF;
5103
5104         EXECUTE 'ALTER TABLE '
5105             || quote_ident(table_name)
5106             || ' DROP COLUMN IF EXISTS x_user';
5107         EXECUTE 'ALTER TABLE '
5108             || quote_ident(table_name)
5109             || ' ADD COLUMN x_user INTEGER';
5110
5111         IF btrim_desired THEN
5112             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
5113                 || ' SET x_user = b.id FROM actor_usr_legacy b'
5114                 || ' WHERE BTRIM(a.' || quote_ident(foreign_column_name)
5115                 || ') = BTRIM(b.' || quote_ident(main_column_name) || ')';
5116         ELSE
5117             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
5118                 || ' SET x_user = b.id FROM actor_usr_legacy b'
5119                 || ' WHERE a.' || quote_ident(foreign_column_name)
5120                 || ' = b.' || quote_ident(main_column_name);
5121         END IF;
5122
5123         --EXECUTE 'SELECT migration_tools.assert(
5124         --    NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE ' || quote_ident(foreign_column_name) || ' <> '''' AND x_user IS NULL),
5125         --    ''Cannot link every barcode'',
5126         --    ''Every barcode linked''
5127         --);';
5128
5129     END;
5130 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5131
5132 -- convenience function for linking two tables
5133 -- e.g. select migration_tools.handle_link(:'migschema','asset_copy','barcode','test_foo','l_barcode','x_acp_id',false);
5134 CREATE OR REPLACE FUNCTION migration_tools.handle_link (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
5135     DECLARE
5136         table_schema ALIAS FOR $1;
5137         table_a ALIAS FOR $2;
5138         column_a ALIAS FOR $3;
5139         table_b ALIAS FOR $4;
5140         column_b ALIAS FOR $5;
5141         column_x ALIAS FOR $6;
5142         btrim_desired ALIAS FOR $7;
5143         proceed BOOLEAN;
5144     BEGIN
5145         EXECUTE 'SELECT EXISTS (
5146             SELECT 1
5147             FROM information_schema.columns
5148             WHERE table_schema = $1
5149             AND table_name = $2
5150             and column_name = $3
5151         )' INTO proceed USING table_schema, table_a, column_a;
5152         IF NOT proceed THEN
5153             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5154         END IF;
5155
5156         EXECUTE 'SELECT EXISTS (
5157             SELECT 1
5158             FROM information_schema.columns
5159             WHERE table_schema = $1
5160             AND table_name = $2
5161             and column_name = $3
5162         )' INTO proceed USING table_schema, table_b, column_b;
5163         IF NOT proceed THEN
5164             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5165         END IF;
5166
5167         EXECUTE 'ALTER TABLE '
5168             || quote_ident(table_b)
5169             || ' DROP COLUMN IF EXISTS ' || quote_ident(column_x);
5170         EXECUTE 'ALTER TABLE '
5171             || quote_ident(table_b)
5172             || ' ADD COLUMN ' || quote_ident(column_x) || ' BIGINT';
5173
5174         IF btrim_desired THEN
5175             EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5176                 || ' SET ' || quote_ident(column_x) || ' = a.id FROM ' || quote_ident(table_a) || ' a'
5177                 || ' WHERE BTRIM(a.' || quote_ident(column_a)
5178                 || ') = BTRIM(b.' || quote_ident(column_b) || ')';
5179         ELSE
5180             EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5181                 || ' SET ' || quote_ident(column_x) || ' = a.id FROM ' || quote_ident(table_a) || ' a'
5182                 || ' WHERE a.' || quote_ident(column_a)
5183                 || ' = b.' || quote_ident(column_b);
5184         END IF;
5185
5186     END;
5187 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5188
5189 -- convenience function for linking two tables, but copying column w into column x instead of "id"
5190 -- e.g. select migration_tools.handle_link2(:'migschema','asset_copy','barcode','test_foo','l_barcode','id','x_acp_id',false);
5191 CREATE OR REPLACE FUNCTION migration_tools.handle_link2 (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,BOOLEAN) RETURNS VOID AS $$
5192     DECLARE
5193         table_schema ALIAS FOR $1;
5194         table_a ALIAS FOR $2;
5195         column_a ALIAS FOR $3;
5196         table_b ALIAS FOR $4;
5197         column_b ALIAS FOR $5;
5198         column_w ALIAS FOR $6;
5199         column_x ALIAS FOR $7;
5200         btrim_desired ALIAS FOR $8;
5201         proceed BOOLEAN;
5202     BEGIN
5203         EXECUTE 'SELECT EXISTS (
5204             SELECT 1
5205             FROM information_schema.columns
5206             WHERE table_schema = $1
5207             AND table_name = $2
5208             and column_name = $3
5209         )' INTO proceed USING table_schema, table_a, column_a;
5210         IF NOT proceed THEN
5211             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5212         END IF;
5213
5214         EXECUTE 'SELECT EXISTS (
5215             SELECT 1
5216             FROM information_schema.columns
5217             WHERE table_schema = $1
5218             AND table_name = $2
5219             and column_name = $3
5220         )' INTO proceed USING table_schema, table_b, column_b;
5221         IF NOT proceed THEN
5222             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5223         END IF;
5224
5225         EXECUTE 'ALTER TABLE '
5226             || quote_ident(table_b)
5227             || ' DROP COLUMN IF EXISTS ' || quote_ident(column_x);
5228         EXECUTE 'ALTER TABLE '
5229             || quote_ident(table_b)
5230             || ' ADD COLUMN ' || quote_ident(column_x) || ' TEXT';
5231
5232         IF btrim_desired THEN
5233             EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5234                 || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5235                 || ' WHERE BTRIM(a.' || quote_ident(column_a)
5236                 || ') = BTRIM(b.' || quote_ident(column_b) || ')';
5237         ELSE
5238             EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5239                 || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5240                 || ' WHERE a.' || quote_ident(column_a)
5241                 || ' = b.' || quote_ident(column_b);
5242         END IF;
5243
5244     END;
5245 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5246
5247 -- 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
5248 -- e.g. select migration_tools.handle_link3(:'migschema','asset_copy','barcode','test_foo','l_barcode','id','x_acp_id');
5249 CREATE OR REPLACE FUNCTION migration_tools.handle_link3 (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5250     DECLARE
5251         table_schema ALIAS FOR $1;
5252         table_a ALIAS FOR $2;
5253         column_a ALIAS FOR $3;
5254         table_b ALIAS FOR $4;
5255         column_b ALIAS FOR $5;
5256         column_w ALIAS FOR $6;
5257         column_x ALIAS FOR $7;
5258         proceed BOOLEAN;
5259     BEGIN
5260         EXECUTE 'SELECT EXISTS (
5261             SELECT 1
5262             FROM information_schema.columns
5263             WHERE table_schema = $1
5264             AND table_name = $2
5265             and column_name = $3
5266         )' INTO proceed USING table_schema, table_a, column_a;
5267         IF NOT proceed THEN
5268             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5269         END IF;
5270
5271         EXECUTE 'SELECT EXISTS (
5272             SELECT 1
5273             FROM information_schema.columns
5274             WHERE table_schema = $1
5275             AND table_name = $2
5276             and column_name = $3
5277         )' INTO proceed USING table_schema, table_b, column_b;
5278         IF NOT proceed THEN
5279             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5280         END IF;
5281
5282         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5283             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5284             || ' WHERE a.' || quote_ident(column_a)
5285             || ' = b.' || quote_ident(column_b);
5286
5287     END;
5288 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5289
5290 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_skip_null_or_empty_string (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5291     DECLARE
5292         table_schema ALIAS FOR $1;
5293         table_a ALIAS FOR $2;
5294         column_a ALIAS FOR $3;
5295         table_b ALIAS FOR $4;
5296         column_b ALIAS FOR $5;
5297         column_w ALIAS FOR $6;
5298         column_x ALIAS FOR $7;
5299         proceed BOOLEAN;
5300     BEGIN
5301         EXECUTE 'SELECT EXISTS (
5302             SELECT 1
5303             FROM information_schema.columns
5304             WHERE table_schema = $1
5305             AND table_name = $2
5306             and column_name = $3
5307         )' INTO proceed USING table_schema, table_a, column_a;
5308         IF NOT proceed THEN
5309             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5310         END IF;
5311
5312         EXECUTE 'SELECT EXISTS (
5313             SELECT 1
5314             FROM information_schema.columns
5315             WHERE table_schema = $1
5316             AND table_name = $2
5317             and column_name = $3
5318         )' INTO proceed USING table_schema, table_b, column_b;
5319         IF NOT proceed THEN
5320             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5321         END IF;
5322
5323         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5324             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5325             || ' WHERE a.' || quote_ident(column_a)
5326             || ' = b.' || quote_ident(column_b)
5327             || ' AND NULLIF(a.' || quote_ident(column_w) || ','''') IS NOT NULL';
5328
5329     END;
5330 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5331
5332 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_skip_null (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5333     DECLARE
5334         table_schema ALIAS FOR $1;
5335         table_a ALIAS FOR $2;
5336         column_a ALIAS FOR $3;
5337         table_b ALIAS FOR $4;
5338         column_b ALIAS FOR $5;
5339         column_w ALIAS FOR $6;
5340         column_x ALIAS FOR $7;
5341         proceed BOOLEAN;
5342     BEGIN
5343         EXECUTE 'SELECT EXISTS (
5344             SELECT 1
5345             FROM information_schema.columns
5346             WHERE table_schema = $1
5347             AND table_name = $2
5348             and column_name = $3
5349         )' INTO proceed USING table_schema, table_a, column_a;
5350         IF NOT proceed THEN
5351             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5352         END IF;
5353
5354         EXECUTE 'SELECT EXISTS (
5355             SELECT 1
5356             FROM information_schema.columns
5357             WHERE table_schema = $1
5358             AND table_name = $2
5359             and column_name = $3
5360         )' INTO proceed USING table_schema, table_b, column_b;
5361         IF NOT proceed THEN
5362             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5363         END IF;
5364
5365         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5366             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5367             || ' WHERE a.' || quote_ident(column_a)
5368             || ' = b.' || quote_ident(column_b)
5369             || ' AND a.' || quote_ident(column_w) || ' IS NOT NULL';
5370
5371     END;
5372 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5373
5374 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_skip_true (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5375     DECLARE
5376         table_schema ALIAS FOR $1;
5377         table_a ALIAS FOR $2;
5378         column_a ALIAS FOR $3;
5379         table_b ALIAS FOR $4;
5380         column_b ALIAS FOR $5;
5381         column_w ALIAS FOR $6;
5382         column_x ALIAS FOR $7;
5383         proceed BOOLEAN;
5384     BEGIN
5385         EXECUTE 'SELECT EXISTS (
5386             SELECT 1
5387             FROM information_schema.columns
5388             WHERE table_schema = $1
5389             AND table_name = $2
5390             and column_name = $3
5391         )' INTO proceed USING table_schema, table_a, column_a;
5392         IF NOT proceed THEN
5393             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5394         END IF;
5395
5396         EXECUTE 'SELECT EXISTS (
5397             SELECT 1
5398             FROM information_schema.columns
5399             WHERE table_schema = $1
5400             AND table_name = $2
5401             and column_name = $3
5402         )' INTO proceed USING table_schema, table_b, column_b;
5403         IF NOT proceed THEN
5404             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5405         END IF;
5406
5407         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5408             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5409             || ' WHERE a.' || quote_ident(column_a)
5410             || ' = b.' || quote_ident(column_b)
5411             || ' AND a.' || quote_ident(column_w) || ' IS NOT TRUE';
5412
5413     END;
5414 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5415
5416 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_skip_false (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5417     DECLARE
5418         table_schema ALIAS FOR $1;
5419         table_a ALIAS FOR $2;
5420         column_a ALIAS FOR $3;
5421         table_b ALIAS FOR $4;
5422         column_b ALIAS FOR $5;
5423         column_w ALIAS FOR $6;
5424         column_x ALIAS FOR $7;
5425         proceed BOOLEAN;
5426     BEGIN
5427         EXECUTE 'SELECT EXISTS (
5428             SELECT 1
5429             FROM information_schema.columns
5430             WHERE table_schema = $1
5431             AND table_name = $2
5432             and column_name = $3
5433         )' INTO proceed USING table_schema, table_a, column_a;
5434         IF NOT proceed THEN
5435             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5436         END IF;
5437
5438         EXECUTE 'SELECT EXISTS (
5439             SELECT 1
5440             FROM information_schema.columns
5441             WHERE table_schema = $1
5442             AND table_name = $2
5443             and column_name = $3
5444         )' INTO proceed USING table_schema, table_b, column_b;
5445         IF NOT proceed THEN
5446             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5447         END IF;
5448
5449         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5450             || ' SET ' || quote_ident(column_x) || ' = a.' || quote_ident(column_w) || ' FROM ' || quote_ident(table_a) || ' a'
5451             || ' WHERE a.' || quote_ident(column_a)
5452             || ' = b.' || quote_ident(column_b)
5453             || ' AND a.' || quote_ident(column_w) || ' IS NOT FALSE';
5454
5455     END;
5456 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5457
5458 CREATE OR REPLACE FUNCTION migration_tools.handle_link3_concat_skip_null (TEXT,TEXT,TEXT,TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5459     DECLARE
5460         table_schema ALIAS FOR $1;
5461         table_a ALIAS FOR $2;
5462         column_a ALIAS FOR $3;
5463         table_b ALIAS FOR $4;
5464         column_b ALIAS FOR $5;
5465         column_w ALIAS FOR $6;
5466         column_x ALIAS FOR $7;
5467         proceed BOOLEAN;
5468     BEGIN
5469         EXECUTE 'SELECT EXISTS (
5470             SELECT 1
5471             FROM information_schema.columns
5472             WHERE table_schema = $1
5473             AND table_name = $2
5474             and column_name = $3
5475         )' INTO proceed USING table_schema, table_a, column_a;
5476         IF NOT proceed THEN
5477             RAISE EXCEPTION '%.% missing column %', table_schema, table_a, column_a; 
5478         END IF;
5479
5480         EXECUTE 'SELECT EXISTS (
5481             SELECT 1
5482             FROM information_schema.columns
5483             WHERE table_schema = $1
5484             AND table_name = $2
5485             and column_name = $3
5486         )' INTO proceed USING table_schema, table_b, column_b;
5487         IF NOT proceed THEN
5488             RAISE EXCEPTION '%.% missing column %', table_schema, table_b, column_b; 
5489         END IF;
5490
5491         EXECUTE 'UPDATE ' || quote_ident(table_b) || ' b'
5492             || ' SET ' || quote_ident(column_x) || ' = CONCAT_WS('' ; '',b.' || quote_ident(column_x) || ',a.' || quote_ident(column_w) || ') FROM ' || quote_ident(table_a) || ' a'
5493             || ' WHERE a.' || quote_ident(column_a)
5494             || ' = b.' || quote_ident(column_b)
5495             || ' AND NULLIF(a.' || quote_ident(column_w) || ','''') IS NOT NULL';
5496
5497     END;
5498 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5499
5500 -- convenience function for handling desired asset stat cats
5501
5502 CREATE OR REPLACE FUNCTION migration_tools.vivicate_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5503     DECLARE
5504         table_schema ALIAS FOR $1;
5505         table_name ALIAS FOR $2;
5506         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
5507         org_shortname ALIAS FOR $4;
5508         proceed BOOLEAN;
5509         org INTEGER;
5510         org_list INTEGER[];
5511         sc TEXT;
5512         sce TEXT;
5513     BEGIN
5514
5515         SELECT 'desired_sc' || field_suffix INTO sc;
5516         SELECT 'desired_sce' || field_suffix INTO sce;
5517
5518         EXECUTE 'SELECT EXISTS (
5519             SELECT 1
5520             FROM information_schema.columns
5521             WHERE table_schema = $1
5522             AND table_name = $2
5523             and column_name = $3
5524         )' INTO proceed USING table_schema, table_name, sc;
5525         IF NOT proceed THEN
5526             RAISE EXCEPTION 'Missing column %', sc; 
5527         END IF;
5528         EXECUTE 'SELECT EXISTS (
5529             SELECT 1
5530             FROM information_schema.columns
5531             WHERE table_schema = $1
5532             AND table_name = $2
5533             and column_name = $3
5534         )' INTO proceed USING table_schema, table_name, sce;
5535         IF NOT proceed THEN
5536             RAISE EXCEPTION 'Missing column %', sce; 
5537         END IF;
5538
5539         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
5540         IF org IS NULL THEN
5541             RAISE EXCEPTION 'Cannot find org by shortname';
5542         END IF;
5543         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
5544
5545         -- caller responsible for their own truncates though we try to prevent duplicates
5546         EXECUTE 'INSERT INTO asset_stat_cat (owner, name)
5547             SELECT DISTINCT
5548                  $1
5549                 ,BTRIM('||sc||')
5550             FROM 
5551                 ' || quote_ident(table_name) || '
5552             WHERE
5553                 NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
5554                 AND NOT EXISTS (
5555                     SELECT id
5556                     FROM asset.stat_cat
5557                     WHERE owner = ANY ($2)
5558                     AND name = BTRIM('||sc||')
5559                 )
5560                 AND NOT EXISTS (
5561                     SELECT id
5562                     FROM asset_stat_cat
5563                     WHERE owner = ANY ($2)
5564                     AND name = BTRIM('||sc||')
5565                 )
5566             ORDER BY 2;'
5567         USING org, org_list;
5568
5569         EXECUTE 'INSERT INTO asset_stat_cat_entry (stat_cat, owner, value)
5570             SELECT DISTINCT
5571                 COALESCE(
5572                     (SELECT id
5573                         FROM asset.stat_cat
5574                         WHERE owner = ANY ($2)
5575                         AND BTRIM('||sc||') = BTRIM(name))
5576                    ,(SELECT id
5577                         FROM asset_stat_cat
5578                         WHERE owner = ANY ($2)
5579                         AND BTRIM('||sc||') = BTRIM(name))
5580                 )
5581                 ,$1
5582                 ,BTRIM('||sce||')
5583             FROM 
5584                 ' || quote_ident(table_name) || '
5585             WHERE
5586                     NULLIF(BTRIM('||sc||'),'''') IS NOT NULL
5587                 AND NULLIF(BTRIM('||sce||'),'''') IS NOT NULL
5588                 AND NOT EXISTS (
5589                     SELECT id
5590                     FROM asset.stat_cat_entry
5591                     WHERE stat_cat = (
5592                         SELECT id
5593                         FROM asset.stat_cat
5594                         WHERE owner = ANY ($2)
5595                         AND BTRIM('||sc||') = BTRIM(name)
5596                     ) AND value = BTRIM('||sce||')
5597                     AND owner = ANY ($2)
5598                 )
5599                 AND NOT EXISTS (
5600                     SELECT id
5601                     FROM asset_stat_cat_entry
5602                     WHERE stat_cat = (
5603                         SELECT id
5604                         FROM asset_stat_cat
5605                         WHERE owner = ANY ($2)
5606                         AND BTRIM('||sc||') = BTRIM(name)
5607                     ) AND value = BTRIM('||sce||')
5608                     AND owner = ANY ($2)
5609                 )
5610             ORDER BY 1,3;'
5611         USING org, org_list;
5612     END;
5613 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5614
5615 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_sc_and_sce (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
5616     DECLARE
5617         table_schema ALIAS FOR $1;
5618         table_name ALIAS FOR $2;
5619         field_suffix ALIAS FOR $3; -- for distinguishing between desired_sce1, desired_sce2, etc.
5620         org_shortname ALIAS FOR $4;
5621         proceed BOOLEAN;
5622         org INTEGER;
5623         org_list INTEGER[];
5624         o INTEGER;
5625         sc TEXT;
5626         sce TEXT;
5627     BEGIN
5628         SELECT 'desired_sc' || field_suffix INTO sc;
5629         SELECT 'desired_sce' || field_suffix INTO sce;
5630         EXECUTE 'SELECT EXISTS (
5631             SELECT 1
5632             FROM information_schema.columns
5633             WHERE table_schema = $1
5634             AND table_name = $2
5635             and column_name = $3
5636         )' INTO proceed USING table_schema, table_name, sc;
5637         IF NOT proceed THEN
5638             RAISE EXCEPTION 'Missing column %', sc; 
5639         END IF;
5640         EXECUTE 'SELECT EXISTS (
5641             SELECT 1
5642             FROM information_schema.columns
5643             WHERE table_schema = $1
5644             AND table_name = $2
5645             and column_name = $3
5646         )' INTO proceed USING table_schema, table_name, sce;
5647         IF NOT proceed THEN
5648             RAISE EXCEPTION 'Missing column %', sce; 
5649         END IF;
5650
5651         SELECT id INTO org FROM actor.org_unit WHERE shortname = org_shortname;
5652         IF org IS NULL THEN
5653             RAISE EXCEPTION 'Cannot find org by shortname';
5654         END IF;
5655
5656         SELECT INTO org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( org );
5657
5658         EXECUTE 'ALTER TABLE '
5659             || quote_ident(table_name)
5660             || ' DROP COLUMN IF EXISTS x_sc' || field_suffix;
5661         EXECUTE 'ALTER TABLE '
5662             || quote_ident(table_name)
5663             || ' ADD COLUMN x_sc' || field_suffix || ' INTEGER';
5664         EXECUTE 'ALTER TABLE '
5665             || quote_ident(table_name)
5666             || ' DROP COLUMN IF EXISTS x_sce' || field_suffix;
5667         EXECUTE 'ALTER TABLE '
5668             || quote_ident(table_name)
5669             || ' ADD COLUMN x_sce' || field_suffix || ' INTEGER';
5670
5671
5672         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
5673             SET
5674                 x_sc' || field_suffix || ' = id
5675             FROM
5676                 (SELECT id, name, owner FROM asset_stat_cat
5677                     UNION SELECT id, name, owner FROM asset.stat_cat) u
5678             WHERE
5679                     BTRIM(UPPER(u.name)) = BTRIM(UPPER(' || sc || '))
5680                 AND u.owner = ANY ($1);'
5681         USING org_list;
5682
5683         EXECUTE 'UPDATE ' || quote_ident(table_name) || '
5684             SET
5685                 x_sce' || field_suffix || ' = id
5686             FROM
5687                 (SELECT id, stat_cat, owner, value FROM asset_stat_cat_entry
5688                     UNION SELECT id, stat_cat, owner, value FROM asset.stat_cat_entry) u
5689             WHERE
5690                     u.stat_cat = x_sc' || field_suffix || '
5691                 AND BTRIM(UPPER(u.value)) = BTRIM(UPPER(' || sce || '))
5692                 AND u.owner = ANY ($1);'
5693         USING org_list;
5694
5695         EXECUTE 'SELECT migration_tools.assert(
5696             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sc' || field_suffix || ' <> '''' AND x_sc' || field_suffix || ' IS NULL),
5697             ''Cannot find a desired stat cat'',
5698             ''Found all desired stat cats''
5699         );';
5700
5701         EXECUTE 'SELECT migration_tools.assert(
5702             NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_sce' || field_suffix || ' <> '''' AND x_sce' || field_suffix || ' IS NULL),
5703             ''Cannot find a desired stat cat entry'',
5704             ''Found all desired stat cat entries''
5705         );';
5706
5707     END;
5708 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
5709
5710 DROP FUNCTION IF EXISTS migration_tools.btrim_lcolumns(TEXT,TEXT);
5711 CREATE OR REPLACE FUNCTION migration_tools.btrim_lcolumns(s_name TEXT, t_name TEXT) RETURNS BOOLEAN
5712  LANGUAGE plpgsql
5713 AS $function$
5714 DECLARE
5715     c_name     TEXT;
5716 BEGIN
5717
5718     FOR c_name IN SELECT column_name FROM information_schema.columns WHERE 
5719             table_name = t_name
5720             AND table_schema = s_name
5721             AND (data_type='text' OR data_type='character varying')
5722             AND column_name like 'l_%'
5723     LOOP
5724        EXECUTE FORMAT('UPDATE ' || s_name || '.' || t_name || ' SET ' || c_name || ' = BTRIM(' || c_name || ')'); 
5725     END LOOP;  
5726
5727     RETURN TRUE;
5728 END
5729 $function$;
5730
5731 DROP FUNCTION IF EXISTS migration_tools.btrim_columns(TEXT,TEXT);
5732 CREATE OR REPLACE FUNCTION migration_tools.btrim_columns(s_name TEXT, t_name TEXT) RETURNS BOOLEAN
5733  LANGUAGE plpgsql
5734 AS $function$
5735 DECLARE
5736     c_name     TEXT;
5737 BEGIN
5738
5739     FOR c_name IN SELECT column_name FROM information_schema.columns WHERE 
5740             table_name = t_name
5741             AND table_schema = s_name
5742             AND (data_type='text' OR data_type='character varying')
5743     LOOP
5744        EXECUTE FORMAT('UPDATE ' || s_name || '.' || t_name || ' SET ' || c_name || ' = BTRIM(' || c_name || ')'); 
5745     END LOOP;  
5746
5747     RETURN TRUE;
5748 END
5749 $function$;
5750
5751 DROP FUNCTION IF EXISTS migration_tools.null_empty_lcolumns(TEXT,TEXT);
5752 CREATE OR REPLACE FUNCTION migration_tools.null_empty_lcolumns(s_name TEXT, t_name TEXT) RETURNS BOOLEAN
5753  LANGUAGE plpgsql
5754 AS $function$
5755 DECLARE
5756     c_name     TEXT;
5757 BEGIN
5758
5759     FOR c_name IN SELECT column_name FROM information_schema.columns WHERE 
5760             table_name = t_name
5761             AND table_schema = s_name
5762             AND (data_type='text' OR data_type='character varying')
5763             AND column_name like 'l_%'
5764     LOOP
5765        EXECUTE FORMAT('UPDATE ' || s_name || '.' || t_name || ' SET ' || c_name || ' = NULL WHERE ' || c_name || ' = '''' '); 
5766     END LOOP;  
5767
5768     RETURN TRUE;
5769 END
5770 $function$;
5771
5772 DROP FUNCTION IF EXISTS migration_tools.null_empty_columns(TEXT,TEXT);
5773 CREATE OR REPLACE FUNCTION migration_tools.null_empty_columns(s_name TEXT, t_name TEXT) RETURNS BOOLEAN
5774  LANGUAGE plpgsql
5775 AS $function$
5776 DECLARE
5777     c_name     TEXT;
5778 BEGIN
5779
5780     FOR c_name IN SELECT column_name FROM information_schema.columns WHERE
5781             table_name = t_name
5782             AND table_schema = s_name
5783             AND (data_type='text' OR data_type='character varying')
5784     LOOP
5785        EXECUTE FORMAT('UPDATE ' || s_name || '.' || t_name || ' SET ' || c_name || ' = NULL WHERE ' || c_name || ' = '''' ');
5786     END LOOP;
5787
5788     RETURN TRUE;
5789 END
5790 $function$;
5791
5792
5793 -- convenience function for handling item barcode collisions in asset_copy_legacy
5794
5795 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
5796 DECLARE
5797     x_barcode TEXT;
5798     x_id BIGINT;
5799     row_count NUMERIC;
5800     internal_collision_count NUMERIC := 0;
5801     incumbent_collision_count NUMERIC := 0;
5802 BEGIN
5803     FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
5804     LOOP
5805         FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
5806         LOOP
5807             UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
5808             GET DIAGNOSTICS row_count = ROW_COUNT;
5809             internal_collision_count := internal_collision_count + row_count;
5810         END LOOP;
5811     END LOOP;
5812     RAISE INFO '% internal collisions', internal_collision_count;
5813     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
5814     LOOP
5815         FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
5816         LOOP
5817             UPDATE asset_copy_legacy SET barcode = migration_schema || '_incumbent_collision_' || id || '_' || barcode WHERE id = x_id;
5818             GET DIAGNOSTICS row_count = ROW_COUNT;
5819             incumbent_collision_count := incumbent_collision_count + row_count;
5820         END LOOP;
5821     END LOOP;
5822     RAISE INFO '% incumbent collisions', incumbent_collision_count;
5823 END
5824 $function$ LANGUAGE plpgsql;
5825
5826 -- convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
5827 -- this should be ran prior to populating actor_card
5828
5829 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
5830 DECLARE
5831     x_barcode TEXT;
5832     x_id BIGINT;
5833     row_count NUMERIC;
5834     internal_collision_count NUMERIC := 0;
5835     incumbent_barcode_collision_count NUMERIC := 0;
5836     incumbent_usrname_collision_count NUMERIC := 0;
5837 BEGIN
5838     FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
5839     LOOP
5840         FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5841         LOOP
5842             UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
5843             GET DIAGNOSTICS row_count = ROW_COUNT;
5844             internal_collision_count := internal_collision_count + row_count;
5845         END LOOP;
5846     END LOOP;
5847     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
5848
5849     FOR x_barcode IN
5850         SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
5851     LOOP
5852         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5853         LOOP
5854             UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_barcode_collision_' || id || '_' || usrname WHERE id = x_id;
5855             GET DIAGNOSTICS row_count = ROW_COUNT;
5856             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
5857         END LOOP;
5858     END LOOP;
5859     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
5860
5861     FOR x_barcode IN
5862         SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
5863     LOOP
5864         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5865         LOOP
5866             UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_usrname_collision_' || id || '_' || usrname WHERE id = x_id;
5867             GET DIAGNOSTICS row_count = ROW_COUNT;
5868             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
5869         END LOOP;
5870     END LOOP;
5871     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
5872 END
5873 $function$ LANGUAGE plpgsql;
5874
5875 -- alternate version: convenience function for handling item barcode collisions in asset_copy_legacy
5876
5877 CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
5878 DECLARE
5879     x_barcode TEXT;
5880     x_id BIGINT;
5881     row_count NUMERIC;
5882     internal_collision_count NUMERIC := 0;
5883     incumbent_collision_count NUMERIC := 0;
5884 BEGIN
5885     FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
5886     LOOP
5887         FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
5888         LOOP
5889             UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
5890             GET DIAGNOSTICS row_count = ROW_COUNT;
5891             internal_collision_count := internal_collision_count + row_count;
5892         END LOOP;
5893     END LOOP;
5894     RAISE INFO '% internal collisions', internal_collision_count;
5895     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
5896     LOOP
5897         FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
5898         LOOP
5899             UPDATE asset_copy_legacy SET barcode = migration_schema || '_' || barcode WHERE id = x_id;
5900             GET DIAGNOSTICS row_count = ROW_COUNT;
5901             incumbent_collision_count := incumbent_collision_count + row_count;
5902         END LOOP;
5903     END LOOP;
5904     RAISE INFO '% incumbent collisions', incumbent_collision_count;
5905 END
5906 $function$ LANGUAGE plpgsql;
5907
5908 -- alternate version: convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
5909 -- this should be ran prior to populating actor_card
5910
5911 CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
5912 DECLARE
5913     x_barcode TEXT;
5914     x_id BIGINT;
5915     row_count NUMERIC;
5916     internal_collision_count NUMERIC := 0;
5917     incumbent_barcode_collision_count NUMERIC := 0;
5918     incumbent_usrname_collision_count NUMERIC := 0;
5919 BEGIN
5920     FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
5921     LOOP
5922         FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5923         LOOP
5924             UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
5925             GET DIAGNOSTICS row_count = ROW_COUNT;
5926             internal_collision_count := internal_collision_count + row_count;
5927         END LOOP;
5928     END LOOP;
5929     RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
5930
5931     FOR x_barcode IN
5932         SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
5933     LOOP
5934         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5935         LOOP
5936             UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
5937             GET DIAGNOSTICS row_count = ROW_COUNT;
5938             incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
5939         END LOOP;
5940     END LOOP;
5941     RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
5942
5943     FOR x_barcode IN
5944         SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
5945     LOOP
5946         FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
5947         LOOP
5948             UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
5949             GET DIAGNOSTICS row_count = ROW_COUNT;
5950             incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
5951         END LOOP;
5952     END LOOP;
5953     RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
5954 END
5955 $function$ LANGUAGE plpgsql;
5956
5957 CREATE OR REPLACE FUNCTION migration_tools.is_circ_rule_safe_to_delete( test_matchpoint INTEGER ) RETURNS BOOLEAN AS $func$
5958 -- WARNING: Use at your own risk
5959 -- FIXME: not considering marc_type, marc_form, marc_bib_level, marc_vr_format, usr_age_lower_bound, usr_age_upper_bound, item_age
5960 DECLARE
5961     item_object asset.copy%ROWTYPE;
5962     user_object actor.usr%ROWTYPE;
5963     test_rule_object config.circ_matrix_matchpoint%ROWTYPE;
5964     result_rule_object config.circ_matrix_matchpoint%ROWTYPE;
5965     safe_to_delete BOOLEAN := FALSE;
5966     m action.found_circ_matrix_matchpoint;
5967     n action.found_circ_matrix_matchpoint;
5968     -- ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] )
5969     result_matchpoint INTEGER;
5970 BEGIN
5971     SELECT INTO test_rule_object * FROM config.circ_matrix_matchpoint WHERE id = test_matchpoint;
5972     RAISE INFO 'testing rule: %', test_rule_object;
5973
5974     INSERT INTO actor.usr (
5975         profile,
5976         usrname,
5977         passwd,
5978         ident_type,
5979         first_given_name,
5980         family_name,
5981         home_ou,
5982         juvenile
5983     ) SELECT
5984         COALESCE(test_rule_object.grp, 2),
5985         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
5986         MD5(NOW()::TEXT),
5987         1,
5988         'Ima',
5989         'Test',
5990         COALESCE(test_rule_object.user_home_ou, test_rule_object.org_unit),
5991         COALESCE(test_rule_object.juvenile_flag, FALSE)
5992     ;
5993     
5994     SELECT INTO user_object * FROM actor.usr WHERE id = currval('actor.usr_id_seq');
5995
5996     INSERT INTO asset.call_number (
5997         creator,
5998         editor,
5999         record,
6000         owning_lib,
6001         label,
6002         label_class
6003     ) SELECT
6004         1,
6005         1,
6006         -1,
6007         COALESCE(test_rule_object.copy_owning_lib,test_rule_object.org_unit),
6008         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
6009         1
6010     ;
6011
6012     INSERT INTO asset.copy (
6013         barcode,
6014         circ_lib,
6015         creator,
6016         call_number,
6017         editor,
6018         location,
6019         loan_duration,
6020         fine_level,
6021         ref,
6022         circ_modifier
6023     ) SELECT
6024         'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
6025         COALESCE(test_rule_object.copy_circ_lib,test_rule_object.org_unit),
6026         1,
6027         currval('asset.call_number_id_seq'),
6028         1,
6029         COALESCE(test_rule_object.copy_location,1),
6030         2,
6031         2,
6032         COALESCE(test_rule_object.ref_flag,FALSE),
6033         test_rule_object.circ_modifier
6034     ;
6035
6036     SELECT INTO item_object * FROM asset.copy WHERE id = currval('asset.copy_id_seq');
6037
6038     SELECT INTO m * FROM action.find_circ_matrix_matchpoint(
6039         test_rule_object.org_unit,
6040         item_object,
6041         user_object,
6042         COALESCE(test_rule_object.is_renewal,FALSE)
6043     );
6044     RAISE INFO '   action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
6045         test_rule_object.org_unit,
6046         item_object.id,
6047         user_object.id,
6048         COALESCE(test_rule_object.is_renewal,FALSE),
6049         m.success,
6050         m.matchpoint,
6051         m.buildrows
6052     ;
6053
6054     --  disable the rule being tested to see if the outcome changes
6055     UPDATE config.circ_matrix_matchpoint SET active = FALSE WHERE id = (m.matchpoint).id;
6056
6057     SELECT INTO n * FROM action.find_circ_matrix_matchpoint(
6058         test_rule_object.org_unit,
6059         item_object,
6060         user_object,
6061         COALESCE(test_rule_object.is_renewal,FALSE)
6062     );
6063     RAISE INFO 'VS action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
6064         test_rule_object.org_unit,
6065         item_object.id,
6066         user_object.id,
6067         COALESCE(test_rule_object.is_renewal,FALSE),
6068         n.success,
6069         n.matchpoint,
6070         n.buildrows
6071     ;
6072
6073     -- FIXME: We could dig deeper and see if the referenced config.rule_*
6074     -- entries are effectively equivalent, but for now, let's assume no
6075     -- duplicate rules at that level
6076     IF (
6077             (m.matchpoint).circulate = (n.matchpoint).circulate
6078         AND (m.matchpoint).duration_rule = (n.matchpoint).duration_rule
6079         AND (m.matchpoint).recurring_fine_rule = (n.matchpoint).recurring_fine_rule
6080         AND (m.matchpoint).max_fine_rule = (n.matchpoint).max_fine_rule
6081         AND (
6082                 (m.matchpoint).hard_due_date = (n.matchpoint).hard_due_date
6083                 OR (
6084                         (m.matchpoint).hard_due_date IS NULL
6085                     AND (n.matchpoint).hard_due_date IS NULL
6086                 )
6087         )
6088         AND (
6089                 (m.matchpoint).renewals = (n.matchpoint).renewals
6090                 OR (
6091                         (m.matchpoint).renewals IS NULL
6092                     AND (n.matchpoint).renewals IS NULL
6093                 )
6094         )
6095         AND (
6096                 (m.matchpoint).grace_period = (n.matchpoint).grace_period
6097                 OR (
6098                         (m.matchpoint).grace_period IS NULL
6099                     AND (n.matchpoint).grace_period IS NULL
6100                 )
6101         )
6102         AND (
6103                 (m.matchpoint).total_copy_hold_ratio = (n.matchpoint).total_copy_hold_ratio
6104                 OR (
6105                         (m.matchpoint).total_copy_hold_ratio IS NULL
6106                     AND (n.matchpoint).total_copy_hold_ratio IS NULL
6107                 )
6108         )
6109         AND (
6110                 (m.matchpoint).available_copy_hold_ratio = (n.matchpoint).available_copy_hold_ratio
6111                 OR (
6112                         (m.matchpoint).available_copy_hold_ratio IS NULL
6113                     AND (n.matchpoint).available_copy_hold_ratio IS NULL
6114                 )
6115         )
6116         AND NOT EXISTS (
6117             SELECT limit_set, fallthrough
6118             FROM config.circ_matrix_limit_set_map
6119             WHERE active and matchpoint = (m.matchpoint).id
6120             EXCEPT
6121             SELECT limit_set, fallthrough
6122             FROM config.circ_matrix_limit_set_map
6123             WHERE active and matchpoint = (n.matchpoint).id
6124         )
6125
6126     ) THEN
6127         RAISE INFO 'rule has same outcome';
6128         safe_to_delete := TRUE;
6129     ELSE
6130         RAISE INFO 'rule has different outcome';
6131         safe_to_delete := FALSE;
6132     END IF;
6133
6134     RAISE EXCEPTION 'rollback the temporary changes';
6135
6136 EXCEPTION WHEN OTHERS THEN
6137
6138     RAISE INFO 'inside exception block: %, %', SQLSTATE, SQLERRM;
6139     RETURN safe_to_delete;
6140
6141 END;
6142 $func$ LANGUAGE plpgsql;
6143