0eca5860fa0f5ac3a0c5c7239d58260759948b01
[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_timestamptz (TEXT,TEXT) RETURNS TIMESTAMPTZ AS $$
654     DECLARE
655         attempt_value ALIAS FOR $1;
656         fail_value ALIAS FOR $2;
657         output TIMESTAMPTZ;
658     BEGIN
659         FOR output IN
660             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::TIMESTAMPTZ 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) || '::TIMESTAMPTZ AS a;'
668             LOOP
669                 RETURN output;
670             END LOOP;
671     END;
672 $$ LANGUAGE PLPGSQL STRICT STABLE;
673
674 CREATE OR REPLACE FUNCTION migration_tools.attempt_money (TEXT,TEXT) RETURNS NUMERIC(8,2) AS $$
675     DECLARE
676         attempt_value ALIAS FOR $1;
677         fail_value ALIAS FOR $2;
678         output NUMERIC(8,2);
679     BEGIN
680         FOR output IN
681             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::NUMERIC(8,2) AS a;'
682         LOOP
683             RETURN output;
684         END LOOP;
685     EXCEPTION
686         WHEN OTHERS THEN
687             FOR output IN
688                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(8,2) AS a;'
689             LOOP
690                 RETURN output;
691             END LOOP;
692     END;
693 $$ LANGUAGE PLPGSQL STRICT STABLE;
694
695 -- add_codabar_checkdigit
696 --   $barcode      source barcode
697 --
698 -- If the source string is 13 or 14 characters long and contains only digits, adds or replaces the 14
699 -- character with a checkdigit computed according to the usual algorithm for library barcodes
700 -- using the Codabar symbology - see <http://www.makebarcode.com/specs/codabar.html>.  If the
701 -- input string does not meet those requirements, it is returned unchanged.
702 --
703 CREATE OR REPLACE FUNCTION migration_tools.add_codabar_checkdigit (TEXT) RETURNS TEXT AS $$
704     my $barcode = shift;
705
706     return $barcode if $barcode !~ /^\d{13,14}$/;
707     $barcode = substr($barcode, 0, 13); # ignore 14th digit
708     my @digits = split //, $barcode;
709     my $total = 0;
710     $total += $digits[$_] foreach (1, 3, 5, 7, 9, 11);
711     $total += (2 * $digits[$_] >= 10) ? (2 * $digits[$_] - 9) : (2 * $digits[$_]) foreach (0, 2, 4, 6, 8, 10, 12);
712     my $remainder = $total % 10;
713     my $checkdigit = ($remainder == 0) ? $remainder : 10 - $remainder;
714     return $barcode . $checkdigit; 
715 $$ LANGUAGE PLPERL STRICT STABLE;
716
717 CREATE OR REPLACE FUNCTION migration_tools.attempt_phone (TEXT,TEXT) RETURNS TEXT AS $$
718   DECLARE
719     phone TEXT := $1;
720     areacode TEXT := $2;
721     temp TEXT := '';
722     output TEXT := '';
723     n_digits INTEGER := 0;
724   BEGIN
725     temp := phone;
726     temp := REGEXP_REPLACE(temp, '^1*[^0-9]*(?=[0-9])', '');
727     temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2-\\3');
728     n_digits := LENGTH(REGEXP_REPLACE(REGEXP_REPLACE(temp, '(.*)?[a-zA-Z].*', E'\\1') , '[^0-9]', '', 'g'));
729     IF n_digits = 7 AND areacode <> '' THEN
730       temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2');
731       output := (areacode || '-' || temp);
732     ELSE
733       output := temp;
734     END IF;
735     RETURN output;
736   END;
737
738 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
739
740 CREATE OR REPLACE FUNCTION migration_tools.set_leader (TEXT, INT, TEXT) RETURNS TEXT AS $$
741   my ($marcxml, $pos, $value) = @_;
742
743   use MARC::Record;
744   use MARC::File::XML;
745
746   my $xml = $marcxml;
747   eval {
748     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
749     my $leader = $marc->leader();
750     substr($leader, $pos, 1) = $value;
751     $marc->leader($leader);
752     $xml = $marc->as_xml_record;
753     $xml =~ s/^<\?.+?\?>$//mo;
754     $xml =~ s/\n//sgo;
755     $xml =~ s/>\s+</></sgo;
756   };
757   return $xml;
758 $$ LANGUAGE PLPERLU STABLE;
759
760 CREATE OR REPLACE FUNCTION migration_tools.set_008 (TEXT, INT, TEXT) RETURNS TEXT AS $$
761   my ($marcxml, $pos, $value) = @_;
762
763   use MARC::Record;
764   use MARC::File::XML;
765
766   my $xml = $marcxml;
767   eval {
768     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
769     my $f008 = $marc->field('008');
770
771     if ($f008) {
772        my $field = $f008->data();
773        substr($field, $pos, 1) = $value;
774        $f008->update($field);
775        $xml = $marc->as_xml_record;
776        $xml =~ s/^<\?.+?\?>$//mo;
777        $xml =~ s/\n//sgo;
778        $xml =~ s/>\s+</></sgo;
779     }
780   };
781   return $xml;
782 $$ LANGUAGE PLPERLU STABLE;
783
784
785 CREATE OR REPLACE FUNCTION migration_tools.is_staff_profile (INT) RETURNS BOOLEAN AS $$
786   DECLARE
787     profile ALIAS FOR $1;
788   BEGIN
789     RETURN CASE WHEN 'Staff' IN (select (permission.grp_ancestors(profile)).name) THEN TRUE ELSE FALSE END;
790   END;
791 $$ LANGUAGE PLPGSQL STRICT STABLE;
792
793
794 CREATE OR REPLACE FUNCTION migration_tools.is_blank (TEXT) RETURNS BOOLEAN AS $$
795   BEGIN
796     RETURN CASE WHEN $1 = '' THEN TRUE ELSE FALSE END;
797   END;
798 $$ LANGUAGE PLPGSQL STRICT STABLE;
799
800
801 CREATE OR REPLACE FUNCTION migration_tools.insert_tags (TEXT, TEXT) RETURNS TEXT AS $$
802
803   my ($marcxml, $tags) = @_;
804
805   use MARC::Record;
806   use MARC::File::XML;
807
808   my $xml = $marcxml;
809
810   eval {
811     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
812     my $to_insert = MARC::Record->new_from_xml("<record>$tags</record>", 'UTF-8');
813
814     my @incumbents = ();
815
816     foreach my $field ( $marc->fields() ) {
817       push @incumbents, $field->as_formatted();
818     }
819
820     foreach $field ( $to_insert->fields() ) {
821       if (!grep {$_ eq $field->as_formatted()} @incumbents) {
822         $marc->insert_fields_ordered( ($field) );
823       }
824     }
825
826     $xml = $marc->as_xml_record;
827     $xml =~ s/^<\?.+?\?>$//mo;
828     $xml =~ s/\n//sgo;
829     $xml =~ s/>\s+</></sgo;
830   };
831
832   return $xml;
833
834 $$ LANGUAGE PLPERLU STABLE;
835
836 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix( tablename TEXT ) RETURNS VOID AS $$
837
838 -- Usage:
839 --
840 --   First make sure the circ matrix is loaded and the circulations
841 --   have been staged to the extent possible (but at the very least
842 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
843 --   circ modifiers must also be in place.
844 --
845 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
846 --
847
848 DECLARE
849   circ_lib             INT;
850   target_copy          INT;
851   usr                  INT;
852   is_renewal           BOOLEAN;
853   this_duration_rule   INT;
854   this_fine_rule       INT;
855   this_max_fine_rule   INT;
856   rcd                  config.rule_circ_duration%ROWTYPE;
857   rrf                  config.rule_recurring_fine%ROWTYPE;
858   rmf                  config.rule_max_fine%ROWTYPE;
859   circ                 INT;
860   n                    INT := 0;
861   n_circs              INT;
862   
863 BEGIN
864
865   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
866
867   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
868
869     -- Fetch the correct rules for this circulation
870     EXECUTE ('
871       SELECT 
872         circ_lib, 
873         target_copy, 
874         usr, 
875         CASE 
876           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE 
877           ELSE FALSE 
878         END 
879       FROM ' || tablename || ' WHERE id = ' || circ || ';') 
880       INTO circ_lib, target_copy, usr, is_renewal ;
881     SELECT 
882       INTO this_duration_rule,
883            this_fine_rule,
884            this_max_fine_rule 
885       duration_rule, 
886       recurring_fine_rule, 
887       max_fine_rule 
888       FROM action.find_circ_matrix_matchpoint( 
889         circ_lib, 
890         target_copy, 
891         usr, 
892         is_renewal 
893         );
894     SELECT INTO rcd * FROM config.rule_circ_duration 
895       WHERE id = this_duration_rule;
896     SELECT INTO rrf * FROM config.rule_recurring_fine 
897       WHERE id = this_fine_rule;
898     SELECT INTO rmf * FROM config.rule_max_fine 
899       WHERE id = this_max_fine_rule;
900
901     -- Apply the rules to this circulation
902     EXECUTE ('UPDATE ' || tablename || ' c
903     SET
904       duration_rule = rcd.name,
905       recurring_fine_rule = rrf.name,
906       max_fine_rule = rmf.name,
907       duration = rcd.normal,
908       recurring_fine = rrf.normal,
909       max_fine = rmf.amount,
910       renewal_remaining = rcd.max_renewals
911     FROM
912       config.rule_circ_duration rcd,
913       config.rule_recurring_fine rrf,
914       config.rule_max_fine rmf
915     WHERE
916       rcd.id = ' || this_duration_rule || ' AND
917       rrf.id = ' || this_fine_rule || ' AND
918       rmf.id = ' || this_max_fine_rule || ' AND
919       c.id = ' || circ || ';');
920
921     -- Keep track of where we are in the process
922     n := n + 1;
923     IF (n % 100 = 0) THEN 
924       RAISE INFO '%', n || ' of ' || n_circs 
925         || ' (' || (100*n/n_circs) || '%) circs updated.';
926     END IF;
927
928   END LOOP;
929
930   RETURN;
931 END;
932
933 $$ LANGUAGE plpgsql;
934
935 CREATE OR REPLACE FUNCTION migration_tools.stage_not_applicable_asset_stat_cats( schemaname TEXT ) RETURNS VOID AS $$
936
937 -- USAGE: Make sure the stat_cat and stat_cat_entry tables are populated, including exactly one 'Not Applicable' entry per stat cat.
938 --        Then SELECT migration_tools.stage_not_applicable_asset_stat_cats('m_foo');
939
940 -- TODO: Make a variant that will go directly to production tables -- which would be useful for retrofixing the absence of N/A cats.
941 -- TODO: Add a similar tool for actor stat cats, which behave differently.
942
943 DECLARE
944         c                    TEXT := schemaname || '.asset_copy_legacy';
945         sc                                                                       TEXT := schemaname || '.asset_stat_cat';
946         sce                                                                      TEXT := schemaname || '.asset_stat_cat_entry';
947         scecm                                                            TEXT := schemaname || '.asset_stat_cat_entry_copy_map';
948         stat_cat                                                 INT;
949   stat_cat_entry       INT;
950   
951 BEGIN
952
953   FOR stat_cat IN EXECUTE ('SELECT id FROM ' || sc) LOOP
954
955                 EXECUTE ('SELECT id FROM ' || sce || ' WHERE stat_cat = ' || stat_cat || E' AND value = \'Not Applicable\';') INTO stat_cat_entry;
956
957                 EXECUTE ('INSERT INTO ' || scecm || ' (owning_copy, stat_cat, stat_cat_entry)
958                                                         SELECT c.id, ' || stat_cat || ', ' || stat_cat_entry || ' FROM ' || c || ' c WHERE c.id NOT IN
959                                                         (SELECT owning_copy FROM ' || scecm || ' WHERE stat_cat = ' || stat_cat || ');');
960
961   END LOOP;
962
963   RETURN;
964 END;
965
966 $$ LANGUAGE plpgsql;
967
968 CREATE OR REPLACE FUNCTION migration_tools.assign_standing_penalties ( ) RETURNS VOID AS $$
969
970 -- USAGE: Once circulation data has been loaded, and group penalty thresholds have been set up, run this.
971 --        This will assign standing penalties as needed.
972
973 DECLARE
974   org_unit  INT;
975   usr       INT;
976
977 BEGIN
978
979   FOR org_unit IN EXECUTE ('SELECT DISTINCT org_unit FROM permission.grp_penalty_threshold;') LOOP
980
981     FOR usr IN EXECUTE ('SELECT id FROM actor.usr WHERE NOT deleted;') LOOP
982   
983       EXECUTE('SELECT actor.calculate_system_penalties(' || usr || ', ' || org_unit || ');');
984
985     END LOOP;
986
987   END LOOP;
988
989   RETURN;
990
991 END;
992
993 $$ LANGUAGE plpgsql;
994
995
996 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_pristine_database () RETURNS VOID AS $$
997
998 BEGIN
999   INSERT INTO metabib.metarecord (fingerprint, master_record)
1000     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
1001       FROM  biblio.record_entry b
1002       WHERE NOT b.deleted
1003         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)
1004         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
1005       ORDER BY b.fingerprint, b.quality DESC;
1006   INSERT INTO metabib.metarecord_source_map (metarecord, source)
1007     SELECT  m.id, r.id
1008       FROM  biblio.record_entry r
1009       JOIN  metabib.metarecord m USING (fingerprint)
1010      WHERE  NOT r.deleted;
1011 END;
1012   
1013 $$ LANGUAGE plpgsql;
1014
1015
1016 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_incumbent_database () RETURNS VOID AS $$
1017
1018 BEGIN
1019   INSERT INTO metabib.metarecord (fingerprint, master_record)
1020     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
1021       FROM  biblio.record_entry b
1022       WHERE NOT b.deleted
1023         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)
1024         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
1025       ORDER BY b.fingerprint, b.quality DESC;
1026   INSERT INTO metabib.metarecord_source_map (metarecord, source)
1027     SELECT  m.id, r.id
1028       FROM  biblio.record_entry r
1029         JOIN metabib.metarecord m USING (fingerprint)
1030       WHERE NOT r.deleted
1031         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);
1032 END;
1033     
1034 $$ LANGUAGE plpgsql;
1035
1036
1037 CREATE OR REPLACE FUNCTION migration_tools.create_cards( schemaname TEXT ) RETURNS VOID AS $$
1038
1039 -- USAGE: Make sure the patrons are staged in schemaname.actor_usr_legacy and have 'usrname' assigned.
1040 --        Then SELECT migration_tools.create_cards('m_foo');
1041
1042 DECLARE
1043         u                    TEXT := schemaname || '.actor_usr_legacy';
1044         c                    TEXT := schemaname || '.actor_card';
1045   
1046 BEGIN
1047
1048         EXECUTE ('TRUNCATE ' || c || ';');
1049         EXECUTE ('INSERT INTO ' || c || ' (usr, barcode) SELECT id, usrname FROM ' || u || ';');
1050         EXECUTE ('UPDATE ' || u || ' u SET card = c.id FROM ' || c || ' c WHERE c.usr = u.id;');
1051
1052   RETURN;
1053
1054 END;
1055
1056 $$ LANGUAGE plpgsql;
1057
1058
1059 CREATE OR REPLACE FUNCTION migration_tools.insert_856_9 (TEXT, TEXT) RETURNS TEXT AS $$
1060
1061   ## USAGE: UPDATE biblio.record_entry SET marc = migration_tools.insert_856_9(marc, 'ABC') WHERE [...];
1062
1063   my ($marcxml, $shortname) = @_;
1064
1065   use MARC::Record;
1066   use MARC::File::XML;
1067
1068   my $xml = $marcxml;
1069
1070   eval {
1071     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
1072
1073     foreach my $field ( $marc->field('856') ) {
1074       if ( scalar(grep( /(netlibrary|overdrive)\.com/i, $field->subfield('u'))) > 0 &&
1075            scalar(grep( $shortname, $field->subfield('9'))) == 0 ) {
1076         $field->add_subfields( '9' => $shortname );
1077                                 $field->update( ind2 => '0');
1078       }
1079     }
1080
1081     $xml = $marc->as_xml_record;
1082     $xml =~ s/^<\?.+?\?>$//mo;
1083     $xml =~ s/\n//sgo;
1084     $xml =~ s/>\s+</></sgo;
1085   };
1086
1087   return $xml;
1088
1089 $$ LANGUAGE PLPERLU STABLE;
1090
1091 CREATE OR REPLACE FUNCTION migration_tools.change_call_number(copy_id BIGINT, new_label TEXT) RETURNS VOID AS $$
1092
1093 DECLARE
1094   old_volume   BIGINT;
1095   new_volume   BIGINT;
1096   bib          BIGINT;
1097   owner        INTEGER;
1098   old_label    TEXT;
1099   remainder    BIGINT;
1100
1101 BEGIN
1102
1103   -- Bail out if asked to change the label to ##URI##
1104   IF new_label = '##URI##' THEN
1105     RETURN;
1106   END IF;
1107
1108   -- Gather information
1109   SELECT call_number INTO old_volume FROM asset.copy WHERE id = copy_id;
1110   SELECT record INTO bib FROM asset.call_number WHERE id = old_volume;
1111   SELECT owning_lib, label INTO owner, old_label FROM asset.call_number WHERE id = old_volume;
1112
1113   -- Bail out if the label already is ##URI##
1114   IF old_label = '##URI##' THEN
1115     RETURN;
1116   END IF;
1117
1118   -- Bail out if the call number label is already correct
1119   IF new_volume = old_volume THEN
1120     RETURN;
1121   END IF;
1122
1123   -- Check whether we already have a destination volume available
1124   SELECT id INTO new_volume FROM asset.call_number 
1125     WHERE 
1126       record = bib AND
1127       owning_lib = owner AND
1128       label = new_label AND
1129       NOT deleted;
1130
1131   -- Create destination volume if needed
1132   IF NOT FOUND THEN
1133     INSERT INTO asset.call_number (creator, editor, record, owning_lib, label) 
1134       VALUES (1, 1, bib, owner, new_label);
1135     SELECT id INTO new_volume FROM asset.call_number
1136       WHERE 
1137         record = bib AND
1138         owning_lib = owner AND
1139         label = new_label AND
1140         NOT deleted;
1141   END IF;
1142
1143   -- Move copy to destination
1144   UPDATE asset.copy SET call_number = new_volume WHERE id = copy_id;
1145
1146   -- Delete source volume if it is now empty
1147   SELECT id INTO remainder FROM asset.copy WHERE call_number = old_volume AND NOT deleted;
1148   IF NOT FOUND THEN
1149     DELETE FROM asset.call_number WHERE id = old_volume;
1150   END IF;
1151
1152 END;
1153
1154 $$ LANGUAGE plpgsql;