Added two MARC-manipulating functions from Galen
[migration-tools.git] / sql / base / base.sql
1 --------------------------------------------------------------------------
2 -- An example of how to use:
3 -- 
4 -- DROP SCHEMA foo CASCADE; CREATE SCHEMA foo; 
5 -- \i base.sql
6 -- SELECT migration_tools.init('foo');
7 -- SELECT migration_tools.build('foo');
8 -- SELECT * FROM foo.fields_requiring_mapping;
9 -- \d foo.actor_usr
10 -- create some incoming ILS specific staging tables, like CREATE foo.legacy_items ( l_barcode TEXT, .. ) INHERITS foo.asset_copy;
11 -- Do some mapping, like UPDATE foo.legacy_items SET barcode = TRIM(BOTH ' ' FROM l_barcode);
12 -- Then, to move into production, do: select migration_tools.insert_base_into_production('foo')
13
14 CREATE SCHEMA migration_tools;
15
16 CREATE OR REPLACE FUNCTION migration_tools.production_tables (TEXT) RETURNS TEXT[] AS $$
17     DECLARE
18         migration_schema ALIAS FOR $1;
19         output  RECORD;
20     BEGIN
21         FOR output IN
22             EXECUTE 'SELECT string_to_array(value,'','') AS tables FROM ' || migration_schema || '.config WHERE key = ''production_tables'';'
23         LOOP
24             RETURN output.tables;
25         END LOOP;
26     END;
27 $$ LANGUAGE PLPGSQL STRICT STABLE;
28
29 CREATE OR REPLACE FUNCTION migration_tools.country_code (TEXT) RETURNS TEXT AS $$
30     DECLARE
31         migration_schema ALIAS FOR $1;
32         output TEXT;
33     BEGIN
34         FOR output IN
35             EXECUTE 'SELECT value FROM ' || migration_schema || '.config WHERE key = ''country_code'';'
36         LOOP
37             RETURN output;
38         END LOOP;
39     END;
40 $$ LANGUAGE PLPGSQL STRICT STABLE;
41
42
43 CREATE OR REPLACE FUNCTION migration_tools.log (TEXT,TEXT,INTEGER) RETURNS VOID AS $$
44     DECLARE
45         migration_schema ALIAS FOR $1;
46         sql ALIAS FOR $2;
47         nrows ALIAS FOR $3;
48     BEGIN
49         EXECUTE 'INSERT INTO ' || migration_schema || '.sql_log ( sql, row_count ) VALUES ( ' || quote_literal(sql) || ', ' || nrows || ' );';
50     END;
51 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
52
53 CREATE OR REPLACE FUNCTION migration_tools.exec (TEXT,TEXT) RETURNS VOID AS $$
54     DECLARE
55         migration_schema ALIAS FOR $1;
56         sql ALIAS FOR $2;
57         nrows INTEGER;
58     BEGIN
59         EXECUTE 'UPDATE ' || migration_schema || '.sql_current SET sql = ' || quote_literal(sql) || ';';
60         --RAISE INFO '%', sql;
61         EXECUTE sql;
62         GET DIAGNOSTICS nrows = ROW_COUNT;
63         PERFORM migration_tools.log(migration_schema,sql,nrows);
64     EXCEPTION
65         WHEN OTHERS THEN 
66             RAISE EXCEPTION '!!!!!!!!!!! state = %, msg = %, sql = %', SQLSTATE, SQLERRM, sql;
67     END;
68 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
69
70 CREATE OR REPLACE FUNCTION migration_tools.debug_exec (TEXT,TEXT) RETURNS VOID AS $$
71     DECLARE
72         migration_schema ALIAS FOR $1;
73         sql ALIAS FOR $2;
74         nrows INTEGER;
75     BEGIN
76         EXECUTE 'UPDATE ' || migration_schema || '.sql_current SET sql = ' || quote_literal(sql) || ';';
77         RAISE INFO 'debug_exec sql = %', sql;
78         EXECUTE sql;
79         GET DIAGNOSTICS nrows = ROW_COUNT;
80         PERFORM migration_tools.log(migration_schema,sql,nrows);
81     EXCEPTION
82         WHEN OTHERS THEN 
83             RAISE EXCEPTION '!!!!!!!!!!! state = %, msg = %, sql = %', SQLSTATE, SQLERRM, sql;
84     END;
85 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
86
87 CREATE OR REPLACE FUNCTION migration_tools.init (TEXT) RETURNS VOID AS $$
88     DECLARE
89         migration_schema ALIAS FOR $1;
90         sql TEXT;
91     BEGIN
92         EXECUTE 'DROP TABLE IF EXISTS ' || migration_schema || '.sql_current;';
93         EXECUTE 'CREATE TABLE ' || migration_schema || '.sql_current ( sql TEXT);';
94         EXECUTE 'INSERT INTO ' || migration_schema || '.sql_current ( sql ) VALUES ( '''' );';
95         BEGIN
96             SELECT 'CREATE TABLE ' || migration_schema || '.sql_log ( time TIMESTAMP NOT NULL DEFAULT NOW(), row_count INTEGER, sql TEXT );' INTO STRICT sql;
97             EXECUTE sql;
98         EXCEPTION
99             WHEN OTHERS THEN 
100                 RAISE INFO '!!!!!!!!!!! state = %, msg = %, sql = %', SQLSTATE, SQLERRM, sql;
101         END;
102         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.config;' );
103         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.config ( key TEXT UNIQUE, value TEXT);' );
104         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''production_tables'', ''asset.call_number,asset.copy_location,asset.copy,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,action.circulation,action.hold_request,action.hold_notification,money.grocery,money.billing,money.cash_payment,money.forgive_payment'' );' );
105         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''country_code'', ''USA'' );' );
106         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.fields_requiring_mapping;' );
107         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.fields_requiring_mapping( table_schema TEXT, table_name TEXT, column_name TEXT, data_type TEXT);' );
108         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_profile_map;' );  
109         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_profile_map ( 
110             id SERIAL,
111             perm_grp_id INTEGER,
112             transcribed_perm_group TEXT,
113             legacy_field1 TEXT,
114             legacy_value1 TEXT,
115             legacy_field2 TEXT,
116             legacy_value2 TEXT,
117             legacy_field3 TEXT,
118             legacy_value3 TEXT
119         );' );
120         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_profile_map'', ''base_profile_map'' );' );
121         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_item_dynamic_field_map;' );  
122         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_item_dynamic_field_map ( 
123             id SERIAL,
124             evergreen_field TEXT,
125             evergreen_value TEXT,
126             evergreen_datatype TEXT,
127             legacy_field1 TEXT,
128             legacy_value1 TEXT,
129             legacy_field2 TEXT,
130             legacy_value2 TEXT,
131             legacy_field3 TEXT,
132             legacy_value3 TEXT
133         );' );
134         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);' ); 
135         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);' ); 
136         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);' ); 
137         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_item_dynamic_field_map'', ''base_item_dynamic_field_map'' );' );
138         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_copy_location_map;' );  
139         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_copy_location_map ( 
140             id SERIAL,
141             location INTEGER,
142             holdable BOOLEAN NOT NULL DEFAULT TRUE,
143             hold_verify BOOLEAN NOT NULL DEFAULT FALSE,
144             opac_visible BOOLEAN NOT NULL DEFAULT TRUE,
145             circulate BOOLEAN NOT NULL DEFAULT TRUE,
146             transcribed_location TEXT,
147             legacy_field1 TEXT,
148             legacy_value1 TEXT,
149             legacy_field2 TEXT,
150             legacy_value2 TEXT,
151             legacy_field3 TEXT,
152             legacy_value3 TEXT
153         );' );
154         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);' ); 
155         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);' ); 
156         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);' ); 
157         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_base_copy_location_loc_idx ON ' || migration_schema || '.base_copy_location_map (transcribed_location);' ); 
158         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_copy_location_map'', ''base_copy_location_map'' );' );
159         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_circ_field_map;' );  
160         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_circ_field_map ( 
161             id SERIAL,
162             circulate BOOLEAN,
163             loan_period TEXT,
164             max_renewals TEXT,
165             max_out TEXT,
166             fine_amount TEXT,
167             fine_interval TEXT,
168             max_fine TEXT,
169             item_field1 TEXT,
170             item_value1 TEXT,
171             item_field2 TEXT,
172             item_value2 TEXT,
173             patron_field1 TEXT,
174             patron_value1 TEXT,
175             patron_field2 TEXT,
176             patron_value2 TEXT
177         );' );
178         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf1_idx ON ' || migration_schema || '.base_circ_field_map (item_field1,item_value1);' ); 
179         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf2_idx ON ' || migration_schema || '.base_circ_field_map (item_field2,item_value2);' ); 
180         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf3_idx ON ' || migration_schema || '.base_circ_field_map (patron_field1,patron_value1);' ); 
181         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf4_idx ON ' || migration_schema || '.base_circ_field_map (patron_field2,patron_value2);' ); 
182         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_circ_field_map'', ''base_circ_field_map'' );' );
183
184         BEGIN
185             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_init'', now() );' );
186         EXCEPTION
187             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_init'';' );
188         END;
189     END;
190 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
191
192 CREATE OR REPLACE FUNCTION migration_tools.build (TEXT) RETURNS VOID AS $$
193     DECLARE
194         migration_schema ALIAS FOR $1;
195         production_tables TEXT[];
196     BEGIN
197         --RAISE INFO 'In migration_tools.build(%)', migration_schema;
198         SELECT migration_tools.production_tables(migration_schema) INTO STRICT production_tables;
199         PERFORM migration_tools.build_base_staging_tables(migration_schema,production_tables);
200         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_patron_barcode_key ON ' || migration_schema || '.actor_card ( barcode );' );
201         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_patron_usrname_key ON ' || migration_schema || '.actor_usr ( usrname );' );
202         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_copy_barcode_key ON ' || migration_schema || '.asset_copy ( barcode );' );
203         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_callnum_record_idx ON ' || migration_schema || '.asset_call_number ( record );' );
204         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 );' );
205         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 );' );
206     END;
207 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
208
209 CREATE OR REPLACE FUNCTION migration_tools.build_base_staging_tables (TEXT,TEXT[]) RETURNS VOID AS $$
210     DECLARE
211         migration_schema ALIAS FOR $1;
212         production_tables ALIAS FOR $2;
213     BEGIN
214         --RAISE INFO 'In migration_tools.build_base_staging_tables(%,%)', migration_schema, production_tables;
215         FOR i IN array_lower(production_tables,1) .. array_upper(production_tables,1) LOOP
216             PERFORM migration_tools.build_specific_base_staging_table(migration_schema,production_tables[i]);
217         END LOOP;
218     END;
219 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
220
221 CREATE OR REPLACE FUNCTION migration_tools.build_specific_base_staging_table (TEXT,TEXT) RETURNS VOID AS $$
222     DECLARE
223         migration_schema ALIAS FOR $1;
224         production_table ALIAS FOR $2;
225         base_staging_table TEXT;
226         columns RECORD;
227     BEGIN
228         base_staging_table = REPLACE( production_table, '.', '_' );
229         --RAISE INFO 'In migration_tools.build_specific_base_staging_table(%,%) -> %', migration_schema, production_table, base_staging_table;
230         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.' || base_staging_table || ' ( LIKE ' || production_table || ' INCLUDING DEFAULTS EXCLUDING CONSTRAINTS );' );
231         PERFORM migration_tools.exec( $1, '
232             INSERT INTO ' || migration_schema || '.fields_requiring_mapping
233                 SELECT table_schema, table_name, column_name, data_type
234                 FROM information_schema.columns 
235                 WHERE table_schema = ''' || migration_schema || ''' AND table_name = ''' || base_staging_table || ''' AND is_nullable = ''NO'' AND column_default IS NULL;
236         ' );
237         FOR columns IN 
238             SELECT table_schema, table_name, column_name, data_type
239             FROM information_schema.columns 
240             WHERE table_schema = migration_schema AND table_name = base_staging_table AND is_nullable = 'NO' AND column_default IS NULL
241         LOOP
242             PERFORM migration_tools.exec( $1, 'ALTER TABLE ' || columns.table_schema || '.' || columns.table_name || ' ALTER COLUMN ' || columns.column_name || ' DROP NOT NULL;' );
243         END LOOP;
244     END;
245 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
246
247 CREATE OR REPLACE FUNCTION migration_tools.insert_base_into_production (TEXT) RETURNS VOID AS $$
248     DECLARE
249         migration_schema ALIAS FOR $1;
250         production_tables TEXT[];
251     BEGIN
252         --RAISE INFO 'In migration_tools.insert_into_production(%)', migration_schema;
253         SELECT migration_tools.production_tables(migration_schema) INTO STRICT production_tables;
254         FOR i IN array_lower(production_tables,1) .. array_upper(production_tables,1) LOOP
255             PERFORM migration_tools.insert_into_production(migration_schema,production_tables[i]);
256         END LOOP;
257     END;
258 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
259
260 CREATE OR REPLACE FUNCTION migration_tools.insert_into_production (TEXT,TEXT) RETURNS VOID AS $$
261     DECLARE
262         migration_schema ALIAS FOR $1;
263         production_table ALIAS FOR $2;
264         base_staging_table TEXT;
265         columns RECORD;
266     BEGIN
267         base_staging_table = REPLACE( production_table, '.', '_' );
268         --RAISE INFO 'In migration_tools.insert_into_production(%,%) -> %', migration_schema, production_table, base_staging_table;
269         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || production_table || ' SELECT * FROM ' || migration_schema || '.' || base_staging_table || ';' );
270     END;
271 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
272
273 CREATE OR REPLACE FUNCTION migration_tools.name_parse_out_last_comma_prefix_first_middle_suffix (TEXT) RETURNS TEXT[] AS $$
274     DECLARE
275         full_name TEXT := $1;
276         temp TEXT;
277         family_name TEXT := '';
278         first_given_name TEXT := '';
279         second_given_name TEXT := '';
280         suffix TEXT := '';
281         prefix TEXT := '';
282     BEGIN
283         temp := full_name;
284         -- Use values, not structure, for prefix/suffix, unless we come up with a better idea
285         IF temp ilike '%MR.%' THEN
286             prefix := 'Mr.';
287             temp := REGEXP_REPLACE( temp, E'MR\.\\s*', '', 'i' );
288         END IF;
289         IF temp ilike '%MRS.%' THEN
290             prefix := 'Mrs.';
291             temp := REGEXP_REPLACE( temp, E'MRS\.\\s*', '', 'i' );
292         END IF;
293         IF temp ilike '%MS.%' THEN
294             prefix := 'Ms.';
295             temp := REGEXP_REPLACE( temp, E'MS\.\\s*', '', 'i' );
296         END IF;
297         IF temp ilike '%DR.%' THEN
298             prefix := 'Dr.';
299             temp := REGEXP_REPLACE( temp, E'DR\.\\s*', '', 'i' );
300         END IF;
301         IF temp ilike '%JR%' THEN
302             suffix := 'Jr.';
303             temp := REGEXP_REPLACE( temp, E'JR\.?\\s*', '', 'i' );
304         END IF;
305         IF temp ilike '%JR,%' THEN
306             suffix := 'Jr.';
307             temp := REGEXP_REPLACE( temp, E'JR,\\s*', ',', 'i' );
308         END IF;
309         IF temp ilike '%SR%' THEN
310             suffix := 'Sr.';
311             temp := REGEXP_REPLACE( temp, E'SR\.?\\s*', '', 'i' );
312         END IF;
313         IF temp ilike '%SR,%' THEN
314             suffix := 'Sr.';
315             temp := REGEXP_REPLACE( temp, E'SR,\\s*', ',', 'i' );
316         END IF;
317         IF temp ~ E'\\sII$' THEN
318             suffix := 'II';
319             temp := REGEXP_REPLACE( temp, E'II$', '', 'i' );
320         END IF;
321         IF temp ~ E'\\sIII$' THEN
322             suffix := 'III';
323             temp := REGEXP_REPLACE( temp, E'III$', '', 'i' );
324         END IF;
325         IF temp ~ E'\\sIV$' THEN
326             suffix := 'IV';
327             temp := REGEXP_REPLACE( temp, E'IV$', '', 'i' );
328         END IF;
329
330         family_name := BTRIM( REGEXP_REPLACE(temp,E'^([^,]*)\\s*,.*$',E'\\1') );
331         first_given_name := BTRIM( CASE WHEN temp ~ ',' THEN REGEXP_REPLACE(temp,E'^[^,]*\\s*,\\s*([^,\\s]*)\\s*.*$',E'\\1') ELSE 'N/A' END );
332         second_given_name := BTRIM( CASE WHEN temp ~ ',' THEN REGEXP_REPLACE(temp,E'^[^,]*\\s*,\\s*[^,\\s]*\\s*(.*)$',E'\\1') ELSE ''  END );
333
334         RETURN ARRAY[ family_name, prefix, first_given_name, second_given_name, suffix ];
335     END;
336 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
337
338 CREATE OR REPLACE FUNCTION migration_tools.address_parse_out_citystatezip (TEXT) RETURNS TEXT[] AS $$
339     DECLARE
340         city_state_zip TEXT := $1;
341         city TEXT := '';
342         state TEXT := '';
343         zip TEXT := '';
344     BEGIN
345         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;
346         city_state_zip := REGEXP_REPLACE( city_state_zip, E'^(.*)\\d\\d\\d\\d\\d-?\\d*(.*)$', E'\\1\\2');
347         IF city_state_zip ~ ',' THEN
348             state := REGEXP_REPLACE( city_state_zip, E'^(.*),(.*)$', E'\\2');
349             city := REGEXP_REPLACE( city_state_zip, E'^(.*),(.*)$', E'\\1');
350         ELSE
351             IF city_state_zip ~ E'\\s+[A-Z][A-Z]\\s*' THEN
352                 state := REGEXP_REPLACE( city_state_zip, E'^.*,?\\s+([A-Z][A-Z])\\s*.*$', E'\\1' );
353                 city := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s+[A-Z][A-Z](\\s*.*)$', E'\\1\\2' );
354             ELSE
355                 IF city_state_zip ~ E'^\\S+$'  THEN
356                     city := city_state_zip;
357                     state := 'N/A';
358                 ELSE
359                     state := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\2');
360                     city := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\1');
361                 END IF;
362             END IF;
363         END IF;
364         RETURN ARRAY[ TRIM(BOTH ' ' FROM city), TRIM(BOTH ' ' FROM state), TRIM(BOTH ' ' FROM zip) ];
365     END;
366 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
367
368 CREATE OR REPLACE FUNCTION migration_tools.rebarcode (o TEXT, t BIGINT) RETURNS TEXT AS $$
369     DECLARE
370         n TEXT := o;
371     BEGIN
372         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
373             IF o::BIGINT < t THEN
374                 n = o::BIGINT + t;
375             END IF;
376         END IF;
377
378         RETURN n;
379     END;
380 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
381
382 CREATE OR REPLACE FUNCTION migration_tools.base_profile_map (TEXT) RETURNS TEXT AS $$
383     DECLARE
384         migration_schema ALIAS FOR $1;
385         output TEXT;
386     BEGIN
387         FOR output IN
388             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_profile_map'';'
389         LOOP
390             RETURN output;
391         END LOOP;
392     END;
393 $$ LANGUAGE PLPGSQL STRICT STABLE;
394
395 CREATE OR REPLACE FUNCTION migration_tools.base_item_dynamic_field_map (TEXT) RETURNS TEXT AS $$
396     DECLARE
397         migration_schema ALIAS FOR $1;
398         output TEXT;
399     BEGIN
400         FOR output IN
401             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_item_dynamic_field_map'';'
402         LOOP
403             RETURN output;
404         END LOOP;
405     END;
406 $$ LANGUAGE PLPGSQL STRICT STABLE;
407
408 CREATE OR REPLACE FUNCTION migration_tools.base_copy_location_map (TEXT) RETURNS TEXT AS $$
409     DECLARE
410         migration_schema ALIAS FOR $1;
411         output TEXT;
412     BEGIN
413         FOR output IN
414             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_copy_location_map'';'
415         LOOP
416             RETURN output;
417         END LOOP;
418     END;
419 $$ LANGUAGE PLPGSQL STRICT STABLE;
420
421 CREATE OR REPLACE FUNCTION migration_tools.base_circ_field_map (TEXT) RETURNS TEXT AS $$
422     DECLARE
423         migration_schema ALIAS FOR $1;
424         output TEXT;
425     BEGIN
426         FOR output IN
427             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_circ_field_map'';'
428         LOOP
429             RETURN output;
430         END LOOP;
431     END;
432 $$ LANGUAGE PLPGSQL STRICT STABLE;
433
434 CREATE OR REPLACE FUNCTION migration_tools.map_base_patron_profile (TEXT,TEXT,INTEGER) RETURNS VOID AS $$
435     DECLARE
436         migration_schema ALIAS FOR $1;
437         profile_map TEXT;
438         patron_table ALIAS FOR $2;
439         default_patron_profile ALIAS FOR $3;
440         sql TEXT;
441         sql_update TEXT;
442         sql_where1 TEXT := '';
443         sql_where2 TEXT := '';
444         sql_where3 TEXT := '';
445         output RECORD;
446     BEGIN
447         SELECT migration_tools.base_profile_map(migration_schema) INTO STRICT profile_map;
448         FOR output IN 
449             EXECUTE 'SELECT * FROM ' || profile_map || E' ORDER BY id;'
450         LOOP
451             sql_update := 'UPDATE ' || patron_table || ' AS u SET profile = perm_grp_id FROM ' || profile_map || ' AS m WHERE ';
452             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);
453             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);
454             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);
455             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,'') || ';';
456             --RAISE INFO 'sql = %', sql;
457             PERFORM migration_tools.exec( $1, sql );
458         END LOOP;
459         PERFORM migration_tools.exec( $1, 'UPDATE ' || patron_table || ' AS u SET profile = ' || quote_literal(default_patron_profile) || ' WHERE profile IS NULL;'  );
460         BEGIN
461             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_patron_mapping_profile'', now() );' );
462         EXCEPTION
463             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_patron_mapping_profile'';' );
464         END;
465     END;
466 $$ LANGUAGE PLPGSQL STRICT STABLE;
467
468 CREATE OR REPLACE FUNCTION migration_tools.map_base_item_table_dynamic (TEXT,TEXT) RETURNS VOID AS $$
469     DECLARE
470         migration_schema ALIAS FOR $1;
471         field_map TEXT;
472         item_table ALIAS FOR $2;
473         sql TEXT;
474         sql_update TEXT;
475         sql_where1 TEXT := '';
476         sql_where2 TEXT := '';
477         sql_where3 TEXT := '';
478         output RECORD;
479     BEGIN
480         SELECT migration_tools.base_item_dynamic_field_map(migration_schema) INTO STRICT field_map;
481         FOR output IN 
482             EXECUTE 'SELECT * FROM ' || field_map || E' ORDER BY id;'
483         LOOP
484             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 ';
485             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);
486             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);
487             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);
488             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,'') || ';';
489             --RAISE INFO 'sql = %', sql;
490             PERFORM migration_tools.exec( $1, sql );
491         END LOOP;
492         BEGIN
493             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_item_mapping_dynamic'', now() );' );
494         EXCEPTION
495             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_item_mapping_dynamic'';' );
496         END;
497     END;
498 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
499
500 CREATE OR REPLACE FUNCTION migration_tools.map_base_item_table_locations (TEXT,TEXT) RETURNS VOID AS $$
501     DECLARE
502         migration_schema ALIAS FOR $1;
503         base_copy_location_map TEXT;
504         item_table ALIAS FOR $2;
505         sql TEXT;
506         sql_update TEXT;
507         sql_where1 TEXT := '';
508         sql_where2 TEXT := '';
509         sql_where3 TEXT := '';
510         output RECORD;
511     BEGIN
512         SELECT migration_tools.base_copy_location_map(migration_schema) INTO STRICT base_copy_location_map;
513         FOR output IN 
514             EXECUTE 'SELECT * FROM ' || base_copy_location_map || E' ORDER BY id;'
515         LOOP
516             sql_update := 'UPDATE ' || item_table || ' AS i SET location = m.location FROM ' || base_copy_location_map || ' AS m WHERE ';
517             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);
518             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);
519             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);
520             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,'') || ';';
521             --RAISE INFO 'sql = %', sql;
522             PERFORM migration_tools.exec( $1, sql );
523         END LOOP;
524         BEGIN
525             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_item_mapping_locations'', now() );' );
526         EXCEPTION
527             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_item_mapping_locations'';' );
528         END;
529     END;
530 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
531
532 -- 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
533 CREATE OR REPLACE FUNCTION migration_tools.map_base_circ_table_dynamic (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
534     DECLARE
535         migration_schema ALIAS FOR $1;
536         field_map TEXT;
537         circ_table ALIAS FOR $2;
538         item_table ALIAS FOR $3;
539         patron_table ALIAS FOR $4;
540         sql TEXT;
541         sql_update TEXT;
542         sql_where1 TEXT := '';
543         sql_where2 TEXT := '';
544         sql_where3 TEXT := '';
545         sql_where4 TEXT := '';
546         output RECORD;
547     BEGIN
548         SELECT migration_tools.base_circ_field_map(migration_schema) INTO STRICT field_map;
549         FOR output IN 
550             EXECUTE 'SELECT * FROM ' || field_map || E' ORDER BY id;'
551         LOOP
552             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 ';
553             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);
554             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);
555             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);
556             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);
557             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,'') || ';';
558             --RAISE INFO 'sql = %', sql;
559             PERFORM migration_tools.exec( $1, sql );
560         END LOOP;
561         BEGIN
562             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_circ_field_mapping'', now() );' );
563         EXCEPTION
564             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_circ_field_mapping'';' );
565         END;
566     END;
567 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
568
569 -- expand_barcode
570 --   $barcode      source barcode
571 --   $prefix       prefix to add to barcode, NULL = add no prefix
572 --   $maxlen       maximum length of barcode; default to 14 if left NULL
573 --   $pad          padding string to apply to left of source barcode before adding
574 --                 prefix and suffix; set to NULL or '' if no padding is desired
575 --   $suffix       suffix to add to barcode, NULL = add no suffix
576 --
577 -- Returns a new string consisting of prefix concatenated with padded barcode and suffix.
578 -- If new barcode would be longer than $maxlen, the original barcode is returned instead.
579 --
580 CREATE OR REPLACE FUNCTION migration_tools.expand_barcode (TEXT, TEXT, INTEGER, TEXT, TEXT) RETURNS TEXT AS $$
581     my ($barcode, $prefix, $maxlen, $pad, $suffix) = @_;
582
583     # default case
584     return unless defined $barcode;
585
586     $prefix     = '' unless defined $prefix;
587     $maxlen ||= 14;
588     $pad        = '0' unless defined $pad;
589     $suffix     = '' unless defined $suffix;
590
591     # bail out if adding prefix and suffix would bring new barcode over max length
592     return $barcode if (length($prefix) + length($barcode) + length($suffix)) > $maxlen;
593
594     my $new_barcode = $barcode;
595     if ($pad ne '') {
596         my $pad_length = $maxlen - length($prefix) - length($suffix);
597         if (length($barcode) < $pad_length) {
598             # assuming we always want padding on the left
599             # also assuming that it is possible to have the pad string be longer than 1 character
600             $new_barcode = substr($pad x ($pad_length - length($barcode)), 0, $pad_length - length($barcode)) . $new_barcode;
601         }
602     }
603
604     # bail out if adding prefix and suffix would bring new barcode over max length
605     return $barcode if (length($prefix) + length($new_barcode) + length($suffix)) > $maxlen;
606
607     return "$prefix$new_barcode$suffix";
608 $$ LANGUAGE PLPERL STABLE;
609
610 CREATE OR REPLACE FUNCTION migration_tools.attempt_cast (TEXT,TEXT,TEXT) RETURNS RECORD AS $$
611     DECLARE
612         attempt_value ALIAS FOR $1;
613         datatype ALIAS FOR $2;
614         fail_value ALIAS FOR $3;
615         output RECORD;
616     BEGIN
617         FOR output IN
618             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::' || datatype || ' AS a;'
619         LOOP
620             RETURN output;
621         END LOOP;
622     EXCEPTION
623         WHEN OTHERS THEN
624             FOR output IN
625                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::' || datatype || ' AS a;'
626             LOOP
627                 RETURN output;
628             END LOOP;
629     END;
630 $$ LANGUAGE PLPGSQL STRICT STABLE;
631
632 CREATE OR REPLACE FUNCTION migration_tools.attempt_date (TEXT,TEXT) RETURNS DATE AS $$
633     DECLARE
634         attempt_value ALIAS FOR $1;
635         fail_value ALIAS FOR $2;
636         output DATE;
637     BEGIN
638         FOR output IN
639             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::date AS a;'
640         LOOP
641             RETURN output;
642         END LOOP;
643     EXCEPTION
644         WHEN OTHERS THEN
645             FOR output IN
646                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::date AS a;'
647             LOOP
648                 RETURN output;
649             END LOOP;
650     END;
651 $$ LANGUAGE PLPGSQL STRICT STABLE;
652
653 CREATE OR REPLACE FUNCTION migration_tools.attempt_money (TEXT,TEXT) RETURNS NUMERIC(8,2) AS $$
654     DECLARE
655         attempt_value ALIAS FOR $1;
656         fail_value ALIAS FOR $2;
657         output NUMERIC(8,2);
658     BEGIN
659         FOR output IN
660             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::NUMERIC(8,2) AS a;'
661         LOOP
662             RETURN output;
663         END LOOP;
664     EXCEPTION
665         WHEN OTHERS THEN
666             FOR output IN
667                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(8,2) AS a;'
668             LOOP
669                 RETURN output;
670             END LOOP;
671     END;
672 $$ LANGUAGE PLPGSQL STRICT STABLE;
673
674 -- add_codabar_checkdigit
675 --   $barcode      source barcode
676 --
677 -- If the source string is 13 or 14 characters long and contains only digits, adds or replaces the 14
678 -- character with a checkdigit computed according to the usual algorithm for library barcodes
679 -- using the Codabar symbology - see <http://www.makebarcode.com/specs/codabar.html>.  If the
680 -- input string does not meet those requirements, it is returned unchanged.
681 --
682 CREATE OR REPLACE FUNCTION migration_tools.add_codabar_checkdigit (TEXT) RETURNS TEXT AS $$
683     my $barcode = shift;
684
685     return $barcode if $barcode !~ /^\d{13,14}$/;
686     $barcode = substr($barcode, 0, 13); # ignore 14th digit
687     my @digits = split //, $barcode;
688     my $total = 0;
689     $total += $digits[$_] foreach (1, 3, 5, 7, 9, 11);
690     $total += (2 * $digits[$_] >= 10) ? (2 * $digits[$_] - 9) : (2 * $digits[$_]) foreach (0, 2, 4, 6, 8, 10, 12);
691     my $remainder = $total % 10;
692     my $checkdigit = ($remainder == 0) ? $remainder : 10 - $remainder;
693     return $barcode . $checkdigit; 
694 $$ LANGUAGE PLPERL STRICT STABLE;
695
696 CREATE OR REPLACE FUNCTION migration_tools.attempt_phone (TEXT,TEXT) RETURNS TEXT AS $$
697   DECLARE
698     phone TEXT := $1;
699     areacode TEXT := $2;
700     temp TEXT := '';
701     output TEXT := '';
702     n_digits INTEGER := 0;
703   BEGIN
704     temp := phone;
705     temp := REGEXP_REPLACE(temp, '^1*[^0-9]*(?=[0-9])', '');
706     temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2-\\3');
707     n_digits := LENGTH(REGEXP_REPLACE(REGEXP_REPLACE(temp, '(.*)?[a-zA-Z].*', E'\\1') , '[^0-9]', '', 'g'));
708     IF n_digits = 7 THEN
709       temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2');
710       output := (areacode || '-' || temp);
711     ELSE
712       output := temp;
713     END IF;
714     RETURN output;
715   END;
716
717 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
718
719 CREATE OR REPLACE FUNCTION migration_tools.set_leader (TEXT, INT, TEXT) RETURNS TEXT AS $$
720   my ($marcxml, $pos, $value) = @_;
721
722   use MARC::Record;
723   use MARC::File::XML;
724
725   my $xml = $marcxml;
726   eval {
727     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
728     my $leader = $marc->leader();
729     substr($leader, $pos, 1) = $value;
730     $marc->leader($leader);
731     $xml = $marc->as_xml_record;
732     $xml =~ s/^<\?.+?\?>$//mo;
733     $xml =~ s/\n//sgo;
734     $xml =~ s/>\s+</></sgo;
735   };
736   return $xml;
737 $$ LANGUAGE PLPERLU STABLE;
738
739 CREATE OR REPLACE FUNCTION migration_tools.set_008 (TEXT, INT, TEXT) RETURNS TEXT AS $$
740   my ($marcxml, $pos, $value) = @_;
741
742   use MARC::Record;
743   use MARC::File::XML;
744
745   my $xml = $marcxml;
746   eval {
747     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
748     my $f008 = $marc->field('008');
749
750     if ($f008) {
751        my $field = $f008->data();
752        substr($field, $pos, 1) = $value;
753        $f008->update($field);
754        $xml = $marc->as_xml_record;
755        $xml =~ s/^<\?.+?\?>$//mo;
756        $xml =~ s/\n//sgo;
757        $xml =~ s/>\s+</></sgo;
758     }
759   };
760   return $xml;
761 $$ LANGUAGE PLPERLU STABLE;
762
763