add routine to calculate Code 39 mod 43 barcode checkdigits
[migration-tools.git] / sql / base / base.sql
1 -- Copyright 2009-2012, Equinox Software, Inc.
2 --
3 -- This program is free software; you can redistribute it and/or
4 -- modify it under the terms of the GNU General Public License
5 -- as published by the Free Software Foundation; either version 2
6 -- of the License, or (at your option) any later version.
7 --
8 -- This program is distributed in the hope that it will be useful,
9 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
10 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 -- GNU General Public License for more details.
12 --
13 -- You should have received a copy of the GNU General Public License
14 -- along with this program; if not, write to the Free Software
15 -- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16
17 --------------------------------------------------------------------------
18 -- An example of how to use:
19 -- 
20 -- DROP SCHEMA foo CASCADE; CREATE SCHEMA foo; 
21 -- \i base.sql
22 -- SELECT migration_tools.init('foo');
23 -- SELECT migration_tools.build('foo');
24 -- SELECT * FROM foo.fields_requiring_mapping;
25 -- \d foo.actor_usr
26 -- create some incoming ILS specific staging tables, like CREATE foo.legacy_items ( l_barcode TEXT, .. ) INHERITS foo.asset_copy;
27 -- Do some mapping, like UPDATE foo.legacy_items SET barcode = TRIM(BOTH ' ' FROM l_barcode);
28 -- Then, to move into production, do: select migration_tools.insert_base_into_production('foo')
29
30 CREATE SCHEMA migration_tools;
31
32 CREATE OR REPLACE FUNCTION migration_tools.production_tables (TEXT) RETURNS TEXT[] AS $$
33     DECLARE
34         migration_schema ALIAS FOR $1;
35         output  RECORD;
36     BEGIN
37         FOR output IN
38             EXECUTE 'SELECT string_to_array(value,'','') AS tables FROM ' || migration_schema || '.config WHERE key = ''production_tables'';'
39         LOOP
40             RETURN output.tables;
41         END LOOP;
42     END;
43 $$ LANGUAGE PLPGSQL STRICT STABLE;
44
45 CREATE OR REPLACE FUNCTION migration_tools.country_code (TEXT) RETURNS TEXT AS $$
46     DECLARE
47         migration_schema ALIAS FOR $1;
48         output TEXT;
49     BEGIN
50         FOR output IN
51             EXECUTE 'SELECT value FROM ' || migration_schema || '.config WHERE key = ''country_code'';'
52         LOOP
53             RETURN output;
54         END LOOP;
55     END;
56 $$ LANGUAGE PLPGSQL STRICT STABLE;
57
58
59 CREATE OR REPLACE FUNCTION migration_tools.log (TEXT,TEXT,INTEGER) RETURNS VOID AS $$
60     DECLARE
61         migration_schema ALIAS FOR $1;
62         sql ALIAS FOR $2;
63         nrows ALIAS FOR $3;
64     BEGIN
65         EXECUTE 'INSERT INTO ' || migration_schema || '.sql_log ( sql, row_count ) VALUES ( ' || quote_literal(sql) || ', ' || nrows || ' );';
66     END;
67 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
68
69 CREATE OR REPLACE FUNCTION migration_tools.exec (TEXT,TEXT) RETURNS VOID AS $$
70     DECLARE
71         migration_schema ALIAS FOR $1;
72         sql ALIAS FOR $2;
73         nrows INTEGER;
74     BEGIN
75         EXECUTE 'UPDATE ' || migration_schema || '.sql_current SET sql = ' || quote_literal(sql) || ';';
76         --RAISE INFO '%', sql;
77         EXECUTE sql;
78         GET DIAGNOSTICS nrows = ROW_COUNT;
79         PERFORM migration_tools.log(migration_schema,sql,nrows);
80     EXCEPTION
81         WHEN OTHERS THEN 
82             RAISE EXCEPTION '!!!!!!!!!!! state = %, msg = %, sql = %', SQLSTATE, SQLERRM, sql;
83     END;
84 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
85
86 CREATE OR REPLACE FUNCTION migration_tools.debug_exec (TEXT,TEXT) RETURNS VOID AS $$
87     DECLARE
88         migration_schema ALIAS FOR $1;
89         sql ALIAS FOR $2;
90         nrows INTEGER;
91     BEGIN
92         EXECUTE 'UPDATE ' || migration_schema || '.sql_current SET sql = ' || quote_literal(sql) || ';';
93         RAISE INFO 'debug_exec sql = %', sql;
94         EXECUTE sql;
95         GET DIAGNOSTICS nrows = ROW_COUNT;
96         PERFORM migration_tools.log(migration_schema,sql,nrows);
97     EXCEPTION
98         WHEN OTHERS THEN 
99             RAISE EXCEPTION '!!!!!!!!!!! state = %, msg = %, sql = %', SQLSTATE, SQLERRM, sql;
100     END;
101 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
102
103 CREATE OR REPLACE FUNCTION migration_tools.init (TEXT) RETURNS VOID AS $$
104     DECLARE
105         migration_schema ALIAS FOR $1;
106         sql TEXT;
107     BEGIN
108         EXECUTE 'DROP TABLE IF EXISTS ' || migration_schema || '.sql_current;';
109         EXECUTE 'CREATE TABLE ' || migration_schema || '.sql_current ( sql TEXT);';
110         EXECUTE 'INSERT INTO ' || migration_schema || '.sql_current ( sql ) VALUES ( '''' );';
111         BEGIN
112             SELECT 'CREATE TABLE ' || migration_schema || '.sql_log ( time TIMESTAMP NOT NULL DEFAULT NOW(), row_count INTEGER, sql TEXT );' INTO STRICT sql;
113             EXECUTE sql;
114         EXCEPTION
115             WHEN OTHERS THEN 
116                 RAISE INFO '!!!!!!!!!!! state = %, msg = %, sql = %', SQLSTATE, SQLERRM, sql;
117         END;
118         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.config;' );
119         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.config ( key TEXT UNIQUE, value TEXT);' );
120         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''production_tables'', ''asset.call_number,asset.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,actor.usr_standing_penalty,action.circulation,action.hold_request,action.hold_notification,money.grocery,money.billing,money.cash_payment,money.forgive_payment,acq.provider,acq.provider_address,acq.provider_note,acq.fund,acq.fund_allocation,acq.fund_tag,acq.funding_source,acq.funding_source_credit,acq.lineitem,acq.purchase_order,acq.po_item,acq.invoice,acq.invoice_item,acq.invoice_entry,acq.lineitem_detail,acq.fund_debit,acq.fund_transfer,acq.po_note'' );' );
121         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''country_code'', ''USA'' );' );
122         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.fields_requiring_mapping;' );
123         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.fields_requiring_mapping( table_schema TEXT, table_name TEXT, column_name TEXT, data_type TEXT);' );
124         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_profile_map;' );  
125         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_profile_map ( 
126             id SERIAL,
127             perm_grp_id INTEGER,
128             transcribed_perm_group TEXT,
129             legacy_field1 TEXT,
130             legacy_value1 TEXT,
131             legacy_field2 TEXT,
132             legacy_value2 TEXT,
133             legacy_field3 TEXT,
134             legacy_value3 TEXT
135         );' );
136         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_profile_map'', ''base_profile_map'' );' );
137         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_item_dynamic_field_map;' );  
138         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_item_dynamic_field_map ( 
139             id SERIAL,
140             evergreen_field TEXT,
141             evergreen_value TEXT,
142             evergreen_datatype TEXT,
143             legacy_field1 TEXT,
144             legacy_value1 TEXT,
145             legacy_field2 TEXT,
146             legacy_value2 TEXT,
147             legacy_field3 TEXT,
148             legacy_value3 TEXT
149         );' );
150         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_item_dynamic_lf1_idx ON ' || migration_schema || '.base_item_dynamic_field_map (legacy_field1,legacy_value1);' ); 
151         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_item_dynamic_lf2_idx ON ' || migration_schema || '.base_item_dynamic_field_map (legacy_field2,legacy_value2);' ); 
152         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_item_dynamic_lf3_idx ON ' || migration_schema || '.base_item_dynamic_field_map (legacy_field3,legacy_value3);' ); 
153         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_item_dynamic_field_map'', ''base_item_dynamic_field_map'' );' );
154         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_copy_location_map;' );  
155         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_copy_location_map ( 
156             id SERIAL,
157             location INTEGER,
158             holdable BOOLEAN NOT NULL DEFAULT TRUE,
159             hold_verify BOOLEAN NOT NULL DEFAULT FALSE,
160             opac_visible BOOLEAN NOT NULL DEFAULT TRUE,
161             circulate BOOLEAN NOT NULL DEFAULT TRUE,
162             transcribed_location TEXT,
163             legacy_field1 TEXT,
164             legacy_value1 TEXT,
165             legacy_field2 TEXT,
166             legacy_value2 TEXT,
167             legacy_field3 TEXT,
168             legacy_value3 TEXT
169         );' );
170         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_base_copy_location_lf1_idx ON ' || migration_schema || '.base_copy_location_map (legacy_field1,legacy_value1);' ); 
171         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_base_copy_location_lf2_idx ON ' || migration_schema || '.base_copy_location_map (legacy_field2,legacy_value2);' ); 
172         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_base_copy_location_lf3_idx ON ' || migration_schema || '.base_copy_location_map (legacy_field3,legacy_value3);' ); 
173         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_base_copy_location_loc_idx ON ' || migration_schema || '.base_copy_location_map (transcribed_location);' ); 
174         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_copy_location_map'', ''base_copy_location_map'' );' );
175         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_circ_field_map;' );  
176         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_circ_field_map ( 
177             id SERIAL,
178             circulate BOOLEAN,
179             loan_period TEXT,
180             max_renewals TEXT,
181             max_out TEXT,
182             fine_amount TEXT,
183             fine_interval TEXT,
184             max_fine TEXT,
185             item_field1 TEXT,
186             item_value1 TEXT,
187             item_field2 TEXT,
188             item_value2 TEXT,
189             patron_field1 TEXT,
190             patron_value1 TEXT,
191             patron_field2 TEXT,
192             patron_value2 TEXT
193         );' );
194         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf1_idx ON ' || migration_schema || '.base_circ_field_map (item_field1,item_value1);' ); 
195         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf2_idx ON ' || migration_schema || '.base_circ_field_map (item_field2,item_value2);' ); 
196         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf3_idx ON ' || migration_schema || '.base_circ_field_map (patron_field1,patron_value1);' ); 
197         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf4_idx ON ' || migration_schema || '.base_circ_field_map (patron_field2,patron_value2);' ); 
198         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_circ_field_map'', ''base_circ_field_map'' );' );
199
200         BEGIN
201             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_init'', now() );' );
202         EXCEPTION
203             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_init'';' );
204         END;
205     END;
206 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
207
208 CREATE OR REPLACE FUNCTION migration_tools.build (TEXT) RETURNS VOID AS $$
209     DECLARE
210         migration_schema ALIAS FOR $1;
211         production_tables TEXT[];
212     BEGIN
213         --RAISE INFO 'In migration_tools.build(%)', migration_schema;
214         SELECT migration_tools.production_tables(migration_schema) INTO STRICT production_tables;
215         PERFORM migration_tools.build_base_staging_tables(migration_schema,production_tables);
216         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_patron_barcode_key ON ' || migration_schema || '.actor_card ( barcode );' );
217         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_patron_usrname_key ON ' || migration_schema || '.actor_usr ( usrname );' );
218         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_copy_barcode_key ON ' || migration_schema || '.asset_copy ( barcode );' );
219         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_copy_id_key ON ' || migration_schema || '.asset_copy ( id );' );
220         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_callnum_record_idx ON ' || migration_schema || '.asset_call_number ( record );' );
221         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_callnum_upper_label_id_lib_idx ON ' || migration_schema || '.asset_call_number ( UPPER(label),id,owning_lib );' );
222         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_callnum_label_once_per_lib ON ' || migration_schema || '.asset_call_number ( record,owning_lib,label );' );
223     END;
224 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
225
226 CREATE OR REPLACE FUNCTION migration_tools.build_base_staging_tables (TEXT,TEXT[]) RETURNS VOID AS $$
227     DECLARE
228         migration_schema ALIAS FOR $1;
229         production_tables ALIAS FOR $2;
230     BEGIN
231         --RAISE INFO 'In migration_tools.build_base_staging_tables(%,%)', migration_schema, production_tables;
232         FOR i IN array_lower(production_tables,1) .. array_upper(production_tables,1) LOOP
233             PERFORM migration_tools.build_specific_base_staging_table(migration_schema,production_tables[i]);
234         END LOOP;
235     END;
236 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
237
238 CREATE OR REPLACE FUNCTION migration_tools.build_specific_base_staging_table (TEXT,TEXT) RETURNS VOID AS $$
239     DECLARE
240         migration_schema ALIAS FOR $1;
241         production_table ALIAS FOR $2;
242         base_staging_table TEXT;
243         columns RECORD;
244     BEGIN
245         base_staging_table = REPLACE( production_table, '.', '_' );
246         --RAISE INFO 'In migration_tools.build_specific_base_staging_table(%,%) -> %', migration_schema, production_table, base_staging_table;
247         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.' || base_staging_table || ' ( LIKE ' || production_table || ' INCLUDING DEFAULTS EXCLUDING CONSTRAINTS );' );
248         PERFORM migration_tools.exec( $1, '
249             INSERT INTO ' || migration_schema || '.fields_requiring_mapping
250                 SELECT table_schema, table_name, column_name, data_type
251                 FROM information_schema.columns 
252                 WHERE table_schema = ''' || migration_schema || ''' AND table_name = ''' || base_staging_table || ''' AND is_nullable = ''NO'' AND column_default IS NULL;
253         ' );
254         FOR columns IN 
255             SELECT table_schema, table_name, column_name, data_type
256             FROM information_schema.columns 
257             WHERE table_schema = migration_schema AND table_name = base_staging_table AND is_nullable = 'NO' AND column_default IS NULL
258         LOOP
259             PERFORM migration_tools.exec( $1, 'ALTER TABLE ' || columns.table_schema || '.' || columns.table_name || ' ALTER COLUMN ' || columns.column_name || ' DROP NOT NULL;' );
260         END LOOP;
261     END;
262 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
263
264 CREATE OR REPLACE FUNCTION migration_tools.insert_base_into_production (TEXT) RETURNS VOID AS $$
265     DECLARE
266         migration_schema ALIAS FOR $1;
267         production_tables TEXT[];
268     BEGIN
269         --RAISE INFO 'In migration_tools.insert_into_production(%)', migration_schema;
270         SELECT migration_tools.production_tables(migration_schema) INTO STRICT production_tables;
271         FOR i IN array_lower(production_tables,1) .. array_upper(production_tables,1) LOOP
272             PERFORM migration_tools.insert_into_production(migration_schema,production_tables[i]);
273         END LOOP;
274     END;
275 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
276
277 CREATE OR REPLACE FUNCTION migration_tools.insert_into_production (TEXT,TEXT) RETURNS VOID AS $$
278     DECLARE
279         migration_schema ALIAS FOR $1;
280         production_table ALIAS FOR $2;
281         base_staging_table TEXT;
282         columns RECORD;
283     BEGIN
284         base_staging_table = REPLACE( production_table, '.', '_' );
285         --RAISE INFO 'In migration_tools.insert_into_production(%,%) -> %', migration_schema, production_table, base_staging_table;
286         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || production_table || ' SELECT * FROM ' || migration_schema || '.' || base_staging_table || ';' );
287     END;
288 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
289
290 CREATE OR REPLACE FUNCTION migration_tools.name_parse_out_last_comma_prefix_first_middle_suffix (TEXT) RETURNS TEXT[] AS $$
291     DECLARE
292         full_name TEXT := $1;
293         temp TEXT;
294         family_name TEXT := '';
295         first_given_name TEXT := '';
296         second_given_name TEXT := '';
297         suffix TEXT := '';
298         prefix TEXT := '';
299     BEGIN
300         temp := full_name;
301         -- Use values, not structure, for prefix/suffix, unless we come up with a better idea
302         IF temp ilike '%MR.%' THEN
303             prefix := 'Mr.';
304             temp := REGEXP_REPLACE( temp, E'MR\.\\s*', '', 'i' );
305         END IF;
306         IF temp ilike '%MRS.%' THEN
307             prefix := 'Mrs.';
308             temp := REGEXP_REPLACE( temp, E'MRS\.\\s*', '', 'i' );
309         END IF;
310         IF temp ilike '%MS.%' THEN
311             prefix := 'Ms.';
312             temp := REGEXP_REPLACE( temp, E'MS\.\\s*', '', 'i' );
313         END IF;
314         IF temp ilike '%DR.%' THEN
315             prefix := 'Dr.';
316             temp := REGEXP_REPLACE( temp, E'DR\.\\s*', '', 'i' );
317         END IF;
318         IF temp ilike '%JR%' THEN
319             suffix := 'Jr.';
320             temp := REGEXP_REPLACE( temp, E'JR\.?\\s*', '', 'i' );
321         END IF;
322         IF temp ilike '%JR,%' THEN
323             suffix := 'Jr.';
324             temp := REGEXP_REPLACE( temp, E'JR,\\s*', ',', 'i' );
325         END IF;
326         IF temp ilike '%SR%' THEN
327             suffix := 'Sr.';
328             temp := REGEXP_REPLACE( temp, E'SR\.?\\s*', '', 'i' );
329         END IF;
330         IF temp ilike '%SR,%' THEN
331             suffix := 'Sr.';
332             temp := REGEXP_REPLACE( temp, E'SR,\\s*', ',', 'i' );
333         END IF;
334         IF temp ~ E'\\sII$' THEN
335             suffix := 'II';
336             temp := REGEXP_REPLACE( temp, E'II$', '', 'i' );
337         END IF;
338         IF temp ~ E'\\sIII$' THEN
339             suffix := 'III';
340             temp := REGEXP_REPLACE( temp, E'III$', '', 'i' );
341         END IF;
342         IF temp ~ E'\\sIV$' THEN
343             suffix := 'IV';
344             temp := REGEXP_REPLACE( temp, E'IV$', '', 'i' );
345         END IF;
346
347         family_name := BTRIM( REGEXP_REPLACE(temp,E'^([^,]*)\\s*,.*$',E'\\1') );
348         first_given_name := BTRIM( CASE WHEN temp ~ ',' THEN REGEXP_REPLACE(temp,E'^[^,]*\\s*,\\s*([^,\\s]*)\\s*.*$',E'\\1') ELSE 'N/A' END );
349         second_given_name := BTRIM( CASE WHEN temp ~ ',' THEN REGEXP_REPLACE(temp,E'^[^,]*\\s*,\\s*[^,\\s]*\\s*(.*)$',E'\\1') ELSE ''  END );
350
351         RETURN ARRAY[ family_name, prefix, first_given_name, second_given_name, suffix ];
352     END;
353 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
354
355 CREATE OR REPLACE FUNCTION migration_tools.address_parse_out_citystatezip (TEXT) RETURNS TEXT[] AS $$
356     DECLARE
357         city_state_zip TEXT := $1;
358         city TEXT := '';
359         state TEXT := '';
360         zip TEXT := '';
361     BEGIN
362         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;
363         city_state_zip := REGEXP_REPLACE( city_state_zip, E'^(.*)\\d\\d\\d\\d\\d-?\\d*(.*)$', E'\\1\\2');
364         IF city_state_zip ~ ',' THEN
365             state := REGEXP_REPLACE( city_state_zip, E'^(.*),(.*)$', E'\\2');
366             city := REGEXP_REPLACE( city_state_zip, E'^(.*),(.*)$', E'\\1');
367         ELSE
368             IF city_state_zip ~ E'\\s+[A-Z][A-Z]\\s*$' THEN
369                 state := REGEXP_REPLACE( city_state_zip, E'^.*,?\\s+([A-Z][A-Z])\\s*$', E'\\1' );
370                 city := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s+[A-Z][A-Z](\\s*)$', E'\\1\\2' );
371             ELSE
372                 IF city_state_zip ~ E'^\\S+$'  THEN
373                     city := city_state_zip;
374                     state := 'N/A';
375                 ELSE
376                     state := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\2');
377                     city := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\1');
378                 END IF;
379             END IF;
380         END IF;
381         RETURN ARRAY[ TRIM(BOTH ' ' FROM city), TRIM(BOTH ' ' FROM state), TRIM(BOTH ' ' FROM zip) ];
382     END;
383 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
384
385 CREATE OR REPLACE FUNCTION migration_tools.rebarcode (o TEXT, t BIGINT) RETURNS TEXT AS $$
386     DECLARE
387         n TEXT := o;
388     BEGIN
389         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
390             IF o::BIGINT < t THEN
391                 n = o::BIGINT + t;
392             END IF;
393         END IF;
394
395         RETURN n;
396     END;
397 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
398
399 CREATE OR REPLACE FUNCTION migration_tools.base_profile_map (TEXT) RETURNS TEXT AS $$
400     DECLARE
401         migration_schema ALIAS FOR $1;
402         output TEXT;
403     BEGIN
404         FOR output IN
405             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_profile_map'';'
406         LOOP
407             RETURN output;
408         END LOOP;
409     END;
410 $$ LANGUAGE PLPGSQL STRICT STABLE;
411
412 CREATE OR REPLACE FUNCTION migration_tools.base_item_dynamic_field_map (TEXT) RETURNS TEXT AS $$
413     DECLARE
414         migration_schema ALIAS FOR $1;
415         output TEXT;
416     BEGIN
417         FOR output IN
418             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_item_dynamic_field_map'';'
419         LOOP
420             RETURN output;
421         END LOOP;
422     END;
423 $$ LANGUAGE PLPGSQL STRICT STABLE;
424
425 CREATE OR REPLACE FUNCTION migration_tools.base_copy_location_map (TEXT) RETURNS TEXT AS $$
426     DECLARE
427         migration_schema ALIAS FOR $1;
428         output TEXT;
429     BEGIN
430         FOR output IN
431             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_copy_location_map'';'
432         LOOP
433             RETURN output;
434         END LOOP;
435     END;
436 $$ LANGUAGE PLPGSQL STRICT STABLE;
437
438 CREATE OR REPLACE FUNCTION migration_tools.base_circ_field_map (TEXT) RETURNS TEXT AS $$
439     DECLARE
440         migration_schema ALIAS FOR $1;
441         output TEXT;
442     BEGIN
443         FOR output IN
444             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_circ_field_map'';'
445         LOOP
446             RETURN output;
447         END LOOP;
448     END;
449 $$ LANGUAGE PLPGSQL STRICT STABLE;
450
451 CREATE OR REPLACE FUNCTION migration_tools.map_base_patron_profile (TEXT,TEXT,INTEGER) RETURNS VOID AS $$
452     DECLARE
453         migration_schema ALIAS FOR $1;
454         profile_map TEXT;
455         patron_table ALIAS FOR $2;
456         default_patron_profile ALIAS FOR $3;
457         sql TEXT;
458         sql_update TEXT;
459         sql_where1 TEXT := '';
460         sql_where2 TEXT := '';
461         sql_where3 TEXT := '';
462         output RECORD;
463     BEGIN
464         SELECT migration_tools.base_profile_map(migration_schema) INTO STRICT profile_map;
465         FOR output IN 
466             EXECUTE 'SELECT * FROM ' || profile_map || E' ORDER BY id;'
467         LOOP
468             sql_update := 'UPDATE ' || patron_table || ' AS u SET profile = perm_grp_id FROM ' || profile_map || ' AS m WHERE ';
469             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);
470             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);
471             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);
472             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,'') || ';';
473             --RAISE INFO 'sql = %', sql;
474             PERFORM migration_tools.exec( $1, sql );
475         END LOOP;
476         PERFORM migration_tools.exec( $1, 'UPDATE ' || patron_table || ' AS u SET profile = ' || quote_literal(default_patron_profile) || ' WHERE profile IS NULL;'  );
477         BEGIN
478             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_patron_mapping_profile'', now() );' );
479         EXCEPTION
480             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_patron_mapping_profile'';' );
481         END;
482     END;
483 $$ LANGUAGE PLPGSQL STRICT STABLE;
484
485 CREATE OR REPLACE FUNCTION migration_tools.map_base_item_table_dynamic (TEXT,TEXT) RETURNS VOID AS $$
486     DECLARE
487         migration_schema ALIAS FOR $1;
488         field_map TEXT;
489         item_table ALIAS FOR $2;
490         sql TEXT;
491         sql_update TEXT;
492         sql_where1 TEXT := '';
493         sql_where2 TEXT := '';
494         sql_where3 TEXT := '';
495         output RECORD;
496     BEGIN
497         SELECT migration_tools.base_item_dynamic_field_map(migration_schema) INTO STRICT field_map;
498         FOR output IN 
499             EXECUTE 'SELECT * FROM ' || field_map || E' ORDER BY id;'
500         LOOP
501             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 ';
502             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);
503             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);
504             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);
505             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,'') || ';';
506             --RAISE INFO 'sql = %', sql;
507             PERFORM migration_tools.exec( $1, sql );
508         END LOOP;
509         BEGIN
510             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_item_mapping_dynamic'', now() );' );
511         EXCEPTION
512             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_item_mapping_dynamic'';' );
513         END;
514     END;
515 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
516
517 CREATE OR REPLACE FUNCTION migration_tools.map_base_item_table_locations (TEXT,TEXT) RETURNS VOID AS $$
518     DECLARE
519         migration_schema ALIAS FOR $1;
520         base_copy_location_map TEXT;
521         item_table ALIAS FOR $2;
522         sql TEXT;
523         sql_update TEXT;
524         sql_where1 TEXT := '';
525         sql_where2 TEXT := '';
526         sql_where3 TEXT := '';
527         output RECORD;
528     BEGIN
529         SELECT migration_tools.base_copy_location_map(migration_schema) INTO STRICT base_copy_location_map;
530         FOR output IN 
531             EXECUTE 'SELECT * FROM ' || base_copy_location_map || E' ORDER BY id;'
532         LOOP
533             sql_update := 'UPDATE ' || item_table || ' AS i SET location = m.location FROM ' || base_copy_location_map || ' AS m WHERE ';
534             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);
535             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);
536             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);
537             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,'') || ';';
538             --RAISE INFO 'sql = %', sql;
539             PERFORM migration_tools.exec( $1, sql );
540         END LOOP;
541         BEGIN
542             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_item_mapping_locations'', now() );' );
543         EXCEPTION
544             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_item_mapping_locations'';' );
545         END;
546     END;
547 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
548
549 -- 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
550 CREATE OR REPLACE FUNCTION migration_tools.map_base_circ_table_dynamic (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
551     DECLARE
552         migration_schema ALIAS FOR $1;
553         field_map TEXT;
554         circ_table ALIAS FOR $2;
555         item_table ALIAS FOR $3;
556         patron_table ALIAS FOR $4;
557         sql TEXT;
558         sql_update TEXT;
559         sql_where1 TEXT := '';
560         sql_where2 TEXT := '';
561         sql_where3 TEXT := '';
562         sql_where4 TEXT := '';
563         output RECORD;
564     BEGIN
565         SELECT migration_tools.base_circ_field_map(migration_schema) INTO STRICT field_map;
566         FOR output IN 
567             EXECUTE 'SELECT * FROM ' || field_map || E' ORDER BY id;'
568         LOOP
569             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 ';
570             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);
571             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);
572             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);
573             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);
574             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,'') || ';';
575             --RAISE INFO 'sql = %', sql;
576             PERFORM migration_tools.exec( $1, sql );
577         END LOOP;
578         BEGIN
579             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_circ_field_mapping'', now() );' );
580         EXCEPTION
581             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_circ_field_mapping'';' );
582         END;
583     END;
584 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
585
586 -- expand_barcode
587 --   $barcode      source barcode
588 --   $prefix       prefix to add to barcode, NULL = add no prefix
589 --   $maxlen       maximum length of barcode; default to 14 if left NULL
590 --   $pad          padding string to apply to left of source barcode before adding
591 --                 prefix and suffix; set to NULL or '' if no padding is desired
592 --   $suffix       suffix to add to barcode, NULL = add no suffix
593 --
594 -- Returns a new string consisting of prefix concatenated with padded barcode and suffix.
595 -- If new barcode would be longer than $maxlen, the original barcode is returned instead.
596 --
597 CREATE OR REPLACE FUNCTION migration_tools.expand_barcode (TEXT, TEXT, INTEGER, TEXT, TEXT) RETURNS TEXT AS $$
598     my ($barcode, $prefix, $maxlen, $pad, $suffix) = @_;
599
600     # default case
601     return unless defined $barcode;
602
603     $prefix     = '' unless defined $prefix;
604     $maxlen ||= 14;
605     $pad        = '0' unless defined $pad;
606     $suffix     = '' unless defined $suffix;
607
608     # bail out if adding prefix and suffix would bring new barcode over max length
609     return $barcode if (length($prefix) + length($barcode) + length($suffix)) > $maxlen;
610
611     my $new_barcode = $barcode;
612     if ($pad ne '') {
613         my $pad_length = $maxlen - length($prefix) - length($suffix);
614         if (length($barcode) < $pad_length) {
615             # assuming we always want padding on the left
616             # also assuming that it is possible to have the pad string be longer than 1 character
617             $new_barcode = substr($pad x ($pad_length - length($barcode)), 0, $pad_length - length($barcode)) . $new_barcode;
618         }
619     }
620
621     # bail out if adding prefix and suffix would bring new barcode over max length
622     return $barcode if (length($prefix) + length($new_barcode) + length($suffix)) > $maxlen;
623
624     return "$prefix$new_barcode$suffix";
625 $$ LANGUAGE PLPERLU STABLE;
626
627 -- remove previous version of this function
628 DROP FUNCTION IF EXISTS migration_tools.attempt_cast(TEXT, TEXT, TEXT);
629
630 CREATE OR REPLACE FUNCTION migration_tools.attempt_cast (TEXT, TEXT) RETURNS TEXT AS $$
631     DECLARE
632         attempt_value ALIAS FOR $1;
633         datatype ALIAS FOR $2;
634     BEGIN
635         EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::' || datatype || ' AS a;';
636         RETURN attempt_value;
637     EXCEPTION
638         WHEN OTHERS THEN RETURN NULL;
639     END;
640 $$ LANGUAGE PLPGSQL STRICT STABLE;
641
642 CREATE OR REPLACE FUNCTION migration_tools.attempt_date (TEXT,TEXT) RETURNS DATE AS $$
643     DECLARE
644         attempt_value ALIAS FOR $1;
645         fail_value ALIAS FOR $2;
646         output DATE;
647     BEGIN
648         FOR output IN
649             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::date AS a;'
650         LOOP
651             RETURN output;
652         END LOOP;
653     EXCEPTION
654         WHEN OTHERS THEN
655             FOR output IN
656                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::date AS a;'
657             LOOP
658                 RETURN output;
659             END LOOP;
660     END;
661 $$ LANGUAGE PLPGSQL STRICT STABLE;
662
663 CREATE OR REPLACE FUNCTION migration_tools.attempt_timestamptz (TEXT,TEXT) RETURNS TIMESTAMPTZ AS $$
664     DECLARE
665         attempt_value ALIAS FOR $1;
666         fail_value ALIAS FOR $2;
667         output TIMESTAMPTZ;
668     BEGIN
669         FOR output IN
670             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::TIMESTAMPTZ AS a;'
671         LOOP
672             RETURN output;
673         END LOOP;
674     EXCEPTION
675         WHEN OTHERS THEN
676             FOR output IN
677                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::TIMESTAMPTZ AS a;'
678             LOOP
679                 RETURN output;
680             END LOOP;
681     END;
682 $$ LANGUAGE PLPGSQL STRICT STABLE;
683
684 CREATE OR REPLACE FUNCTION migration_tools.attempt_money (TEXT,TEXT) RETURNS NUMERIC(8,2) AS $$
685     DECLARE
686         attempt_value ALIAS FOR $1;
687         fail_value ALIAS FOR $2;
688         output NUMERIC(8,2);
689     BEGIN
690         FOR output IN
691             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::NUMERIC(8,2) AS a;'
692         LOOP
693             RETURN output;
694         END LOOP;
695     EXCEPTION
696         WHEN OTHERS THEN
697             FOR output IN
698                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(8,2) AS a;'
699             LOOP
700                 RETURN output;
701             END LOOP;
702     END;
703 $$ LANGUAGE PLPGSQL STRICT STABLE;
704
705 -- add_codabar_checkdigit
706 --   $barcode      source barcode
707 --
708 -- If the source string is 13 or 14 characters long and contains only digits, adds or replaces the 14
709 -- character with a checkdigit computed according to the usual algorithm for library barcodes
710 -- using the Codabar symbology - see <http://www.makebarcode.com/specs/codabar.html>.  If the
711 -- input string does not meet those requirements, it is returned unchanged.
712 --
713 CREATE OR REPLACE FUNCTION migration_tools.add_codabar_checkdigit (TEXT) RETURNS TEXT AS $$
714     my $barcode = shift;
715
716     return $barcode if $barcode !~ /^\d{13,14}$/;
717     $barcode = substr($barcode, 0, 13); # ignore 14th digit
718     my @digits = split //, $barcode;
719     my $total = 0;
720     $total += $digits[$_] foreach (1, 3, 5, 7, 9, 11);
721     $total += (2 * $digits[$_] >= 10) ? (2 * $digits[$_] - 9) : (2 * $digits[$_]) foreach (0, 2, 4, 6, 8, 10, 12);
722     my $remainder = $total % 10;
723     my $checkdigit = ($remainder == 0) ? $remainder : 10 - $remainder;
724     return $barcode . $checkdigit; 
725 $$ LANGUAGE PLPERLU STRICT STABLE;
726
727 -- add_code39mod43_checkdigit
728 --   $barcode      source barcode
729 --
730 -- If the source string is 13 or 14 characters long and contains only valid
731 -- Code 39 mod 43 characters, adds or replaces the 14th
732 -- character with a checkdigit computed according to the usual algorithm for library barcodes
733 -- using the Code 39 mod 43 symbology - see <http://en.wikipedia.org/wiki/Code_39#Code_39_mod_43>.  If the
734 -- input string does not meet those requirements, it is returned unchanged.
735 --
736 CREATE OR REPLACE FUNCTION migration_tools.add_code39mod43_checkdigit (TEXT) RETURNS TEXT AS $$
737     my $barcode = shift;
738
739     return $barcode if $barcode !~ /^[0-9A-Z. $\/+%-]{13,14}$/;
740     $barcode = substr($barcode, 0, 13); # ignore 14th character
741
742     my @valid_chars = split //, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%';
743     my %nums = map { $valid_chars[$_] => $_ } (0..42);
744
745     my $total = 0;
746     $total += $nums{$_} foreach split(//, $barcode);
747     my $remainder = $total % 43;
748     my $checkdigit = $valid_chars[$remainder];
749     return $barcode . $checkdigit;
750 $$ LANGUAGE PLPERLU STRICT STABLE;
751
752 CREATE OR REPLACE FUNCTION migration_tools.attempt_phone (TEXT,TEXT) RETURNS TEXT AS $$
753   DECLARE
754     phone TEXT := $1;
755     areacode TEXT := $2;
756     temp TEXT := '';
757     output TEXT := '';
758     n_digits INTEGER := 0;
759   BEGIN
760     temp := phone;
761     temp := REGEXP_REPLACE(temp, '^1*[^0-9]*(?=[0-9])', '');
762     temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2-\\3');
763     n_digits := LENGTH(REGEXP_REPLACE(REGEXP_REPLACE(temp, '(.*)?[a-zA-Z].*', E'\\1') , '[^0-9]', '', 'g'));
764     IF n_digits = 7 AND areacode <> '' THEN
765       temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2');
766       output := (areacode || '-' || temp);
767     ELSE
768       output := temp;
769     END IF;
770     RETURN output;
771   END;
772
773 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
774
775 CREATE OR REPLACE FUNCTION migration_tools.set_leader (TEXT, INT, TEXT) RETURNS TEXT AS $$
776   my ($marcxml, $pos, $value) = @_;
777
778   use MARC::Record;
779   use MARC::File::XML;
780
781   my $xml = $marcxml;
782   eval {
783     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
784     my $leader = $marc->leader();
785     substr($leader, $pos, 1) = $value;
786     $marc->leader($leader);
787     $xml = $marc->as_xml_record;
788     $xml =~ s/^<\?.+?\?>$//mo;
789     $xml =~ s/\n//sgo;
790     $xml =~ s/>\s+</></sgo;
791   };
792   return $xml;
793 $$ LANGUAGE PLPERLU STABLE;
794
795 CREATE OR REPLACE FUNCTION migration_tools.set_008 (TEXT, INT, TEXT) RETURNS TEXT AS $$
796   my ($marcxml, $pos, $value) = @_;
797
798   use MARC::Record;
799   use MARC::File::XML;
800
801   my $xml = $marcxml;
802   eval {
803     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
804     my $f008 = $marc->field('008');
805
806     if ($f008) {
807        my $field = $f008->data();
808        substr($field, $pos, 1) = $value;
809        $f008->update($field);
810        $xml = $marc->as_xml_record;
811        $xml =~ s/^<\?.+?\?>$//mo;
812        $xml =~ s/\n//sgo;
813        $xml =~ s/>\s+</></sgo;
814     }
815   };
816   return $xml;
817 $$ LANGUAGE PLPERLU STABLE;
818
819
820 CREATE OR REPLACE FUNCTION migration_tools.is_staff_profile (INT) RETURNS BOOLEAN AS $$
821   DECLARE
822     profile ALIAS FOR $1;
823   BEGIN
824     RETURN CASE WHEN 'Staff' IN (select (permission.grp_ancestors(profile)).name) THEN TRUE ELSE FALSE END;
825   END;
826 $$ LANGUAGE PLPGSQL STRICT STABLE;
827
828
829 CREATE OR REPLACE FUNCTION migration_tools.is_blank (TEXT) RETURNS BOOLEAN AS $$
830   BEGIN
831     RETURN CASE WHEN $1 = '' THEN TRUE ELSE FALSE END;
832   END;
833 $$ LANGUAGE PLPGSQL STRICT STABLE;
834
835
836 CREATE OR REPLACE FUNCTION migration_tools.insert_tags (TEXT, TEXT) RETURNS TEXT AS $$
837
838   my ($marcxml, $tags) = @_;
839
840   use MARC::Record;
841   use MARC::File::XML;
842
843   my $xml = $marcxml;
844
845   eval {
846     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
847     my $to_insert = MARC::Record->new_from_xml("<record>$tags</record>", 'UTF-8');
848
849     my @incumbents = ();
850
851     foreach my $field ( $marc->fields() ) {
852       push @incumbents, $field->as_formatted();
853     }
854
855     foreach $field ( $to_insert->fields() ) {
856       if (!grep {$_ eq $field->as_formatted()} @incumbents) {
857         $marc->insert_fields_ordered( ($field) );
858       }
859     }
860
861     $xml = $marc->as_xml_record;
862     $xml =~ s/^<\?.+?\?>$//mo;
863     $xml =~ s/\n//sgo;
864     $xml =~ s/>\s+</></sgo;
865   };
866
867   return $xml;
868
869 $$ LANGUAGE PLPERLU STABLE;
870
871 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix( tablename TEXT ) RETURNS VOID AS $$
872
873 -- Usage:
874 --
875 --   First make sure the circ matrix is loaded and the circulations
876 --   have been staged to the extent possible (but at the very least
877 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
878 --   circ modifiers must also be in place.
879 --
880 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
881 --
882
883 DECLARE
884   circ_lib             INT;
885   target_copy          INT;
886   usr                  INT;
887   is_renewal           BOOLEAN;
888   this_duration_rule   INT;
889   this_fine_rule       INT;
890   this_max_fine_rule   INT;
891   rcd                  config.rule_circ_duration%ROWTYPE;
892   rrf                  config.rule_recurring_fine%ROWTYPE;
893   rmf                  config.rule_max_fine%ROWTYPE;
894   circ                 INT;
895   n                    INT := 0;
896   n_circs              INT;
897   
898 BEGIN
899
900   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
901
902   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
903
904     -- Fetch the correct rules for this circulation
905     EXECUTE ('
906       SELECT
907         circ_lib,
908         target_copy,
909         usr,
910         CASE
911           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
912           ELSE FALSE
913         END
914       FROM ' || tablename || ' WHERE id = ' || circ || ';')
915       INTO circ_lib, target_copy, usr, is_renewal ;
916     SELECT
917       INTO this_duration_rule,
918            this_fine_rule,
919            this_max_fine_rule
920       duration_rule,
921       recurring_fine_rule,
922       max_fine_rule
923       FROM action.item_user_circ_test(
924         circ_lib,
925         target_copy,
926         usr,
927         is_renewal
928         );
929     SELECT INTO rcd * FROM config.rule_circ_duration
930       WHERE id = this_duration_rule;
931     SELECT INTO rrf * FROM config.rule_recurring_fine
932       WHERE id = this_fine_rule;
933     SELECT INTO rmf * FROM config.rule_max_fine
934       WHERE id = this_max_fine_rule;
935
936     -- Apply the rules to this circulation
937     EXECUTE ('UPDATE ' || tablename || ' c
938     SET
939       duration_rule = rcd.name,
940       recurring_fine_rule = rrf.name,
941       max_fine_rule = rmf.name,
942       duration = rcd.normal,
943       recurring_fine = rrf.normal,
944       max_fine =
945         CASE rmf.is_percent
946           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
947           ELSE rmf.amount
948         END,
949       renewal_remaining = rcd.max_renewals
950     FROM
951       config.rule_circ_duration rcd,
952       config.rule_recurring_fine rrf,
953       config.rule_max_fine rmf,
954                         asset.copy ac
955     WHERE
956       rcd.id = ' || this_duration_rule || ' AND
957       rrf.id = ' || this_fine_rule || ' AND
958       rmf.id = ' || this_max_fine_rule || ' AND
959                         ac.id = c.target_copy AND
960       c.id = ' || circ || ';');
961
962     -- Keep track of where we are in the process
963     n := n + 1;
964     IF (n % 100 = 0) THEN
965       RAISE INFO '%', n || ' of ' || n_circs
966         || ' (' || (100*n/n_circs) || '%) circs updated.';
967     END IF;
968
969   END LOOP;
970
971   RETURN;
972 END;
973
974 $$ LANGUAGE plpgsql;
975
976 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix_before_20( tablename TEXT ) RETURNS VOID AS $$
977
978 -- Usage:
979 --
980 --   First make sure the circ matrix is loaded and the circulations
981 --   have been staged to the extent possible (but at the very least
982 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
983 --   circ modifiers must also be in place.
984 --
985 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
986 --
987
988 DECLARE
989   circ_lib             INT;
990   target_copy          INT;
991   usr                  INT;
992   is_renewal           BOOLEAN;
993   this_duration_rule   INT;
994   this_fine_rule       INT;
995   this_max_fine_rule   INT;
996   rcd                  config.rule_circ_duration%ROWTYPE;
997   rrf                  config.rule_recurring_fine%ROWTYPE;
998   rmf                  config.rule_max_fine%ROWTYPE;
999   circ                 INT;
1000   n                    INT := 0;
1001   n_circs              INT;
1002   
1003 BEGIN
1004
1005   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
1006
1007   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
1008
1009     -- Fetch the correct rules for this circulation
1010     EXECUTE ('
1011       SELECT
1012         circ_lib,
1013         target_copy,
1014         usr,
1015         CASE
1016           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
1017           ELSE FALSE
1018         END
1019       FROM ' || tablename || ' WHERE id = ' || circ || ';')
1020       INTO circ_lib, target_copy, usr, is_renewal ;
1021     SELECT
1022       INTO this_duration_rule,
1023            this_fine_rule,
1024            this_max_fine_rule
1025       duration_rule,
1026       recuring_fine_rule,
1027       max_fine_rule
1028       FROM action.find_circ_matrix_matchpoint(
1029         circ_lib,
1030         target_copy,
1031         usr,
1032         is_renewal
1033         );
1034     SELECT INTO rcd * FROM config.rule_circ_duration
1035       WHERE id = this_duration_rule;
1036     SELECT INTO rrf * FROM config.rule_recurring_fine
1037       WHERE id = this_fine_rule;
1038     SELECT INTO rmf * FROM config.rule_max_fine
1039       WHERE id = this_max_fine_rule;
1040
1041     -- Apply the rules to this circulation
1042     EXECUTE ('UPDATE ' || tablename || ' c
1043     SET
1044       duration_rule = rcd.name,
1045       recuring_fine_rule = rrf.name,
1046       max_fine_rule = rmf.name,
1047       duration = rcd.normal,
1048       recuring_fine = rrf.normal,
1049       max_fine =
1050         CASE rmf.is_percent
1051           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
1052           ELSE rmf.amount
1053         END,
1054       renewal_remaining = rcd.max_renewals
1055     FROM
1056       config.rule_circ_duration rcd,
1057       config.rule_recuring_fine rrf,
1058       config.rule_max_fine rmf,
1059                         asset.copy ac
1060     WHERE
1061       rcd.id = ' || this_duration_rule || ' AND
1062       rrf.id = ' || this_fine_rule || ' AND
1063       rmf.id = ' || this_max_fine_rule || ' AND
1064                         ac.id = c.target_copy AND
1065       c.id = ' || circ || ';');
1066
1067     -- Keep track of where we are in the process
1068     n := n + 1;
1069     IF (n % 100 = 0) THEN
1070       RAISE INFO '%', n || ' of ' || n_circs
1071         || ' (' || (100*n/n_circs) || '%) circs updated.';
1072     END IF;
1073
1074   END LOOP;
1075
1076   RETURN;
1077 END;
1078
1079 $$ LANGUAGE plpgsql;
1080
1081 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix_after_20( tablename TEXT ) RETURNS VOID AS $$
1082
1083 -- Usage:
1084 --
1085 --   First make sure the circ matrix is loaded and the circulations
1086 --   have been staged to the extent possible (but at the very least
1087 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
1088 --   circ modifiers must also be in place.
1089 --
1090 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
1091 --
1092
1093 DECLARE
1094   circ_lib             INT;
1095   target_copy          INT;
1096   usr                  INT;
1097   is_renewal           BOOLEAN;
1098   this_duration_rule   INT;
1099   this_fine_rule       INT;
1100   this_max_fine_rule   INT;
1101   rcd                  config.rule_circ_duration%ROWTYPE;
1102   rrf                  config.rule_recurring_fine%ROWTYPE;
1103   rmf                  config.rule_max_fine%ROWTYPE;
1104   circ                 INT;
1105   n                    INT := 0;
1106   n_circs              INT;
1107   
1108 BEGIN
1109
1110   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
1111
1112   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
1113
1114     -- Fetch the correct rules for this circulation
1115     EXECUTE ('
1116       SELECT
1117         circ_lib,
1118         target_copy,
1119         usr,
1120         CASE
1121           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
1122           ELSE FALSE
1123         END
1124       FROM ' || tablename || ' WHERE id = ' || circ || ';')
1125       INTO circ_lib, target_copy, usr, is_renewal ;
1126     SELECT
1127       INTO this_duration_rule,
1128            this_fine_rule,
1129            this_max_fine_rule
1130       (matchpoint).duration_rule,
1131       (matchpoint).recurring_fine_rule,
1132       (matchpoint).max_fine_rule
1133       FROM action.find_circ_matrix_matchpoint(
1134         circ_lib,
1135         target_copy,
1136         usr,
1137         is_renewal
1138         );
1139     SELECT INTO rcd * FROM config.rule_circ_duration
1140       WHERE id = this_duration_rule;
1141     SELECT INTO rrf * FROM config.rule_recurring_fine
1142       WHERE id = this_fine_rule;
1143     SELECT INTO rmf * FROM config.rule_max_fine
1144       WHERE id = this_max_fine_rule;
1145
1146     -- Apply the rules to this circulation
1147     EXECUTE ('UPDATE ' || tablename || ' c
1148     SET
1149       duration_rule = rcd.name,
1150       recurring_fine_rule = rrf.name,
1151       max_fine_rule = rmf.name,
1152       duration = rcd.normal,
1153       recurring_fine = rrf.normal,
1154       max_fine =
1155         CASE rmf.is_percent
1156           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
1157           ELSE rmf.amount
1158         END,
1159       renewal_remaining = rcd.max_renewals,
1160       grace_period = rrf.grace_period
1161     FROM
1162       config.rule_circ_duration rcd,
1163       config.rule_recurring_fine rrf,
1164       config.rule_max_fine rmf,
1165                         asset.copy ac
1166     WHERE
1167       rcd.id = ' || this_duration_rule || ' AND
1168       rrf.id = ' || this_fine_rule || ' AND
1169       rmf.id = ' || this_max_fine_rule || ' AND
1170                         ac.id = c.target_copy AND
1171       c.id = ' || circ || ';');
1172
1173     -- Keep track of where we are in the process
1174     n := n + 1;
1175     IF (n % 100 = 0) THEN
1176       RAISE INFO '%', n || ' of ' || n_circs
1177         || ' (' || (100*n/n_circs) || '%) circs updated.';
1178     END IF;
1179
1180   END LOOP;
1181
1182   RETURN;
1183 END;
1184
1185 $$ LANGUAGE plpgsql;
1186
1187
1188
1189
1190 CREATE OR REPLACE FUNCTION migration_tools.stage_not_applicable_asset_stat_cats( schemaname TEXT ) RETURNS VOID AS $$
1191
1192 -- USAGE: Make sure the stat_cat and stat_cat_entry tables are populated, including exactly one 'Not Applicable' entry per stat cat.
1193 --        Then SELECT migration_tools.stage_not_applicable_asset_stat_cats('m_foo');
1194
1195 -- TODO: Make a variant that will go directly to production tables -- which would be useful for retrofixing the absence of N/A cats.
1196 -- TODO: Add a similar tool for actor stat cats, which behave differently.
1197
1198 DECLARE
1199         c                    TEXT := schemaname || '.asset_copy_legacy';
1200         sc                                                                       TEXT := schemaname || '.asset_stat_cat';
1201         sce                                                                      TEXT := schemaname || '.asset_stat_cat_entry';
1202         scecm                                                            TEXT := schemaname || '.asset_stat_cat_entry_copy_map';
1203         stat_cat                                                 INT;
1204   stat_cat_entry       INT;
1205   
1206 BEGIN
1207
1208   FOR stat_cat IN EXECUTE ('SELECT id FROM ' || sc) LOOP
1209
1210                 EXECUTE ('SELECT id FROM ' || sce || ' WHERE stat_cat = ' || stat_cat || E' AND value = \'Not Applicable\';') INTO stat_cat_entry;
1211
1212                 EXECUTE ('INSERT INTO ' || scecm || ' (owning_copy, stat_cat, stat_cat_entry)
1213                                                         SELECT c.id, ' || stat_cat || ', ' || stat_cat_entry || ' FROM ' || c || ' c WHERE c.id NOT IN
1214                                                         (SELECT owning_copy FROM ' || scecm || ' WHERE stat_cat = ' || stat_cat || ');');
1215
1216   END LOOP;
1217
1218   RETURN;
1219 END;
1220
1221 $$ LANGUAGE plpgsql;
1222
1223 CREATE OR REPLACE FUNCTION migration_tools.assign_standing_penalties ( ) RETURNS VOID AS $$
1224
1225 -- USAGE: Once circulation data has been loaded, and group penalty thresholds have been set up, run this.
1226 --        This will assign standing penalties as needed.
1227
1228 DECLARE
1229   org_unit  INT;
1230   usr       INT;
1231
1232 BEGIN
1233
1234   FOR org_unit IN EXECUTE ('SELECT DISTINCT org_unit FROM permission.grp_penalty_threshold;') LOOP
1235
1236     FOR usr IN EXECUTE ('SELECT id FROM actor.usr WHERE NOT deleted;') LOOP
1237   
1238       EXECUTE('SELECT actor.calculate_system_penalties(' || usr || ', ' || org_unit || ');');
1239
1240     END LOOP;
1241
1242   END LOOP;
1243
1244   RETURN;
1245
1246 END;
1247
1248 $$ LANGUAGE plpgsql;
1249
1250
1251 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_pristine_database () RETURNS VOID AS $$
1252
1253 BEGIN
1254   INSERT INTO metabib.metarecord (fingerprint, master_record)
1255     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
1256       FROM  biblio.record_entry b
1257       WHERE NOT b.deleted
1258         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)
1259         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
1260       ORDER BY b.fingerprint, b.quality DESC;
1261   INSERT INTO metabib.metarecord_source_map (metarecord, source)
1262     SELECT  m.id, r.id
1263       FROM  biblio.record_entry r
1264       JOIN  metabib.metarecord m USING (fingerprint)
1265      WHERE  NOT r.deleted;
1266 END;
1267   
1268 $$ LANGUAGE plpgsql;
1269
1270
1271 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_incumbent_database () RETURNS VOID AS $$
1272
1273 BEGIN
1274   INSERT INTO metabib.metarecord (fingerprint, master_record)
1275     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
1276       FROM  biblio.record_entry b
1277       WHERE NOT b.deleted
1278         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)
1279         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
1280       ORDER BY b.fingerprint, b.quality DESC;
1281   INSERT INTO metabib.metarecord_source_map (metarecord, source)
1282     SELECT  m.id, r.id
1283       FROM  biblio.record_entry r
1284         JOIN metabib.metarecord m USING (fingerprint)
1285       WHERE NOT r.deleted
1286         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);
1287 END;
1288     
1289 $$ LANGUAGE plpgsql;
1290
1291
1292 CREATE OR REPLACE FUNCTION migration_tools.create_cards( schemaname TEXT ) RETURNS VOID AS $$
1293
1294 -- USAGE: Make sure the patrons are staged in schemaname.actor_usr_legacy and have 'usrname' assigned.
1295 --        Then SELECT migration_tools.create_cards('m_foo');
1296
1297 DECLARE
1298         u                    TEXT := schemaname || '.actor_usr_legacy';
1299         c                    TEXT := schemaname || '.actor_card';
1300   
1301 BEGIN
1302
1303         EXECUTE ('DELETE FROM ' || c || ';');
1304         EXECUTE ('INSERT INTO ' || c || ' (usr, barcode) SELECT id, usrname FROM ' || u || ';');
1305         EXECUTE ('UPDATE ' || u || ' u SET card = c.id FROM ' || c || ' c WHERE c.usr = u.id;');
1306
1307   RETURN;
1308
1309 END;
1310
1311 $$ LANGUAGE plpgsql;
1312
1313
1314 CREATE OR REPLACE FUNCTION migration_tools.insert_856_9 (TEXT, TEXT) RETURNS TEXT AS $$
1315
1316   ## USAGE: UPDATE biblio.record_entry SET marc = migration_tools.insert_856_9(marc, 'ABC') WHERE [...];
1317
1318   my ($marcxml, $shortname) = @_;
1319
1320   use MARC::Record;
1321   use MARC::File::XML;
1322
1323   my $xml = $marcxml;
1324
1325   eval {
1326     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
1327
1328     foreach my $field ( $marc->field('856') ) {
1329       if ( scalar(grep( /(contentreserve|netlibrary|overdrive)\.com/i, $field->subfield('u'))) > 0 &&
1330            ! ( $field->as_string('9') =~ m/$shortname/ ) ) {
1331         $field->add_subfields( '9' => $shortname );
1332                                 $field->update( ind2 => '0');
1333       }
1334     }
1335
1336     $xml = $marc->as_xml_record;
1337     $xml =~ s/^<\?.+?\?>$//mo;
1338     $xml =~ s/\n//sgo;
1339     $xml =~ s/>\s+</></sgo;
1340   };
1341
1342   return $xml;
1343
1344 $$ LANGUAGE PLPERLU STABLE;
1345
1346 CREATE OR REPLACE FUNCTION migration_tools.change_call_number(copy_id BIGINT, new_label TEXT, cn_class BIGINT) RETURNS VOID AS $$
1347
1348 DECLARE
1349   old_volume   BIGINT;
1350   new_volume   BIGINT;
1351   bib          BIGINT;
1352   owner        INTEGER;
1353   old_label    TEXT;
1354   remainder    BIGINT;
1355
1356 BEGIN
1357
1358   -- Bail out if asked to change the label to ##URI##
1359   IF new_label = '##URI##' THEN
1360     RETURN;
1361   END IF;
1362
1363   -- Gather information
1364   SELECT call_number INTO old_volume FROM asset.copy WHERE id = copy_id;
1365   SELECT record INTO bib FROM asset.call_number WHERE id = old_volume;
1366   SELECT owning_lib, label INTO owner, old_label FROM asset.call_number WHERE id = old_volume;
1367
1368   -- Bail out if the label already is ##URI##
1369   IF old_label = '##URI##' THEN
1370     RETURN;
1371   END IF;
1372
1373   -- Bail out if the call number label is already correct
1374   IF new_volume = old_volume THEN
1375     RETURN;
1376   END IF;
1377
1378   -- Check whether we already have a destination volume available
1379   SELECT id INTO new_volume FROM asset.call_number 
1380     WHERE 
1381       record = bib AND
1382       owning_lib = owner AND
1383       label = new_label AND
1384       NOT deleted;
1385
1386   -- Create destination volume if needed
1387   IF NOT FOUND THEN
1388     INSERT INTO asset.call_number (creator, editor, record, owning_lib, label, label_class) 
1389       VALUES (1, 1, bib, owner, new_label, cn_class);
1390     SELECT id INTO new_volume FROM asset.call_number
1391       WHERE 
1392         record = bib AND
1393         owning_lib = owner AND
1394         label = new_label AND
1395         NOT deleted;
1396   END IF;
1397
1398   -- Move copy to destination
1399   UPDATE asset.copy SET call_number = new_volume WHERE id = copy_id;
1400
1401   -- Delete source volume if it is now empty
1402   SELECT id INTO remainder FROM asset.copy WHERE call_number = old_volume AND NOT deleted;
1403   IF NOT FOUND THEN
1404     DELETE FROM asset.call_number WHERE id = old_volume;
1405   END IF;
1406
1407 END;
1408
1409 $$ LANGUAGE plpgsql;
1410
1411 CREATE OR REPLACE FUNCTION migration_tools.zip_to_city_state_county (TEXT) RETURNS TEXT[] AS $$
1412
1413         my $input = $_[0];
1414         my %zipdata;
1415
1416         open (FH, '<', '/openils/var/data/zips.txt') or return ('No File Found', 'No File Found', 'No File Found');
1417
1418         while (<FH>) {
1419                 chomp;
1420                 my ($junk, $state, $city, $zip, $foo, $bar, $county, $baz, $morejunk) = split(/\|/);
1421                 $zipdata{$zip} = [$city, $state, $county];
1422         }
1423
1424         if (defined $zipdata{$input}) {
1425                 my ($city, $state, $county) = @{$zipdata{$input}};
1426                 return [$city, $state, $county];
1427         } elsif (defined $zipdata{substr $input, 0, 5}) {
1428                 my ($city, $state, $county) = @{$zipdata{substr $input, 0, 5}};
1429                 return [$city, $state, $county];
1430         } else {
1431                 return ['ZIP not found', 'ZIP not found', 'ZIP not found'];
1432         }
1433   
1434 $$ LANGUAGE PLPERLU STABLE;
1435
1436 CREATE OR REPLACE FUNCTION migration_tools.check_ou_depth ( ) RETURNS VOID AS $$
1437
1438 DECLARE
1439   ou  INT;
1440         org_unit_depth INT;
1441         ou_parent INT;
1442         parent_depth INT;
1443   errors_found BOOLEAN;
1444         ou_shortname TEXT;
1445         parent_shortname TEXT;
1446         ou_type_name TEXT;
1447         parent_type TEXT;
1448         type_id INT;
1449         type_depth INT;
1450         type_parent INT;
1451         type_parent_depth INT;
1452         proper_parent TEXT;
1453
1454 BEGIN
1455
1456         errors_found := FALSE;
1457
1458 -- Checking actor.org_unit_type
1459
1460         FOR type_id IN EXECUTE ('SELECT id FROM actor.org_unit_type ORDER BY id;') LOOP
1461
1462                 SELECT depth FROM actor.org_unit_type WHERE id = type_id INTO type_depth;
1463                 SELECT parent FROM actor.org_unit_type WHERE id = type_id INTO type_parent;
1464
1465                 IF type_parent IS NOT NULL THEN
1466
1467                         SELECT depth FROM actor.org_unit_type WHERE id = type_parent INTO type_parent_depth;
1468
1469                         IF type_depth - type_parent_depth <> 1 THEN
1470                                 SELECT name FROM actor.org_unit_type WHERE id = type_id INTO ou_type_name;
1471                                 SELECT name FROM actor.org_unit_type WHERE id = type_parent INTO parent_type;
1472                                 RAISE INFO 'The % org unit type has a depth of %, but its parent org unit type, %, has a depth of %.',
1473                                         ou_type_name, type_depth, parent_type, type_parent_depth;
1474                                 errors_found := TRUE;
1475
1476                         END IF;
1477
1478                 END IF;
1479
1480         END LOOP;
1481
1482 -- Checking actor.org_unit
1483
1484   FOR ou IN EXECUTE ('SELECT id FROM actor.org_unit ORDER BY shortname;') LOOP
1485
1486                 SELECT parent_ou FROM actor.org_unit WHERE id = ou INTO ou_parent;
1487                 SELECT t.depth FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou INTO org_unit_depth;
1488                 SELECT t.depth FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou_parent INTO parent_depth;
1489                 SELECT shortname FROM actor.org_unit WHERE id = ou INTO ou_shortname;
1490                 SELECT shortname FROM actor.org_unit WHERE id = ou_parent INTO parent_shortname;
1491                 SELECT t.name FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou INTO ou_type_name;
1492                 SELECT t.name FROM actor.org_unit_type t, actor.org_unit o WHERE o.ou_type = t.id and o.id = ou_parent INTO parent_type;
1493
1494                 IF ou_parent IS NOT NULL THEN
1495
1496                         IF      (org_unit_depth - parent_depth <> 1) OR (
1497                                 (SELECT parent FROM actor.org_unit_type WHERE name = ou_type_name) <> (SELECT id FROM actor.org_unit_type WHERE name = parent_type)
1498                         ) THEN
1499                                 RAISE INFO '% (org unit %) is a % (depth %) but its parent, % (org unit %), is a % (depth %).', 
1500                                         ou_shortname, ou, ou_type_name, org_unit_depth, parent_shortname, ou_parent, parent_type, parent_depth;
1501                                 errors_found := TRUE;
1502                         END IF;
1503
1504                 END IF;
1505
1506   END LOOP;
1507
1508         IF NOT errors_found THEN
1509                 RAISE INFO 'No errors found.';
1510         END IF;
1511
1512   RETURN;
1513
1514 END;
1515
1516 $$ LANGUAGE plpgsql;
1517
1518
1519 CREATE OR REPLACE FUNCTION migration_tools.refresh_opac_visible_copies ( ) RETURNS VOID AS $$
1520
1521 BEGIN   
1522
1523         DELETE FROM asset.opac_visible_copies;
1524
1525         INSERT INTO asset.opac_visible_copies (id, circ_lib, record)
1526                 SELECT DISTINCT
1527                         cp.id, cp.circ_lib, cn.record
1528                 FROM
1529                         asset.copy cp
1530                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
1531                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
1532                         JOIN asset.copy_location cl ON (cp.location = cl.id)
1533                         JOIN config.copy_status cs ON (cp.status = cs.id)
1534                         JOIN biblio.record_entry b ON (cn.record = b.id)
1535                 WHERE 
1536                         NOT cp.deleted AND
1537                         NOT cn.deleted AND
1538                         NOT b.deleted AND
1539                         cs.opac_visible AND
1540                         cl.opac_visible AND
1541                         cp.opac_visible AND
1542                         a.opac_visible AND
1543                         cp.id NOT IN (SELECT id FROM asset.opac_visible_copies);
1544
1545 END;
1546
1547 $$ LANGUAGE plpgsql;
1548
1549
1550 CREATE OR REPLACE FUNCTION migration_tools.change_owning_lib(copy_id BIGINT, new_owning_lib INTEGER) RETURNS VOID AS $$
1551
1552 DECLARE
1553   old_volume     BIGINT;
1554   new_volume     BIGINT;
1555   bib            BIGINT;
1556   old_owning_lib INTEGER;
1557         old_label      TEXT;
1558   remainder      BIGINT;
1559
1560 BEGIN
1561
1562   -- Gather information
1563   SELECT call_number INTO old_volume FROM asset.copy WHERE id = copy_id;
1564   SELECT record INTO bib FROM asset.call_number WHERE id = old_volume;
1565   SELECT owning_lib, label INTO old_owning_lib, old_label FROM asset.call_number WHERE id = old_volume;
1566
1567         -- Bail out if the new_owning_lib is not the ID of an org_unit
1568         IF new_owning_lib NOT IN (SELECT id FROM actor.org_unit) THEN
1569                 RAISE WARNING 
1570                         '% is not a valid actor.org_unit ID; no change made.', 
1571                                 new_owning_lib;
1572                 RETURN;
1573         END IF;
1574
1575   -- Bail out discreetly if the owning_lib is already correct
1576   IF new_owning_lib = old_owning_lib THEN
1577     RETURN;
1578   END IF;
1579
1580   -- Check whether we already have a destination volume available
1581   SELECT id INTO new_volume FROM asset.call_number 
1582     WHERE 
1583       record = bib AND
1584       owning_lib = new_owning_lib AND
1585       label = old_label AND
1586       NOT deleted;
1587
1588   -- Create destination volume if needed
1589   IF NOT FOUND THEN
1590     INSERT INTO asset.call_number (creator, editor, record, owning_lib, label) 
1591       VALUES (1, 1, bib, new_owning_lib, old_label);
1592     SELECT id INTO new_volume FROM asset.call_number
1593       WHERE 
1594         record = bib AND
1595         owning_lib = new_owning_lib AND
1596         label = old_label AND
1597         NOT deleted;
1598   END IF;
1599
1600   -- Move copy to destination
1601   UPDATE asset.copy SET call_number = new_volume WHERE id = copy_id;
1602
1603   -- Delete source volume if it is now empty
1604   SELECT id INTO remainder FROM asset.copy WHERE call_number = old_volume AND NOT deleted;
1605   IF NOT FOUND THEN
1606     DELETE FROM asset.call_number WHERE id = old_volume;
1607   END IF;
1608
1609 END;
1610
1611 $$ LANGUAGE plpgsql;
1612
1613
1614 CREATE OR REPLACE FUNCTION migration_tools.change_owning_lib(copy_id BIGINT, new_owner TEXT) RETURNS VOID AS $$
1615
1616 -- You can use shortnames with this function, which looks up the org unit ID and passes it to change_owning_lib(BIGINT,INTEGER).
1617
1618 DECLARE
1619         new_owning_lib  INTEGER;
1620
1621 BEGIN
1622
1623         -- Parse the new_owner as an org unit ID or shortname
1624         IF new_owner IN (SELECT shortname FROM actor.org_unit) THEN
1625                 SELECT id INTO new_owning_lib FROM actor.org_unit WHERE shortname = new_owner;
1626                 PERFORM migration_tools.change_owning_lib(copy_id, new_owning_lib);
1627         ELSIF new_owner ~ E'^[0-9]+$' THEN
1628                 IF new_owner::INTEGER IN (SELECT id FROM actor.org_unit) THEN
1629                         RAISE INFO 
1630                                 '%',
1631                                 E'You don\'t need to put the actor.org_unit ID in quotes; '
1632                                         || E'if you put it in quotes, I\'m going to try to parse it as a shortname first.';
1633                         new_owning_lib := new_owner::INTEGER;
1634                 PERFORM migration_tools.change_owning_lib(copy_id, new_owning_lib);
1635                 END IF;
1636         ELSE
1637                 RAISE WARNING 
1638                         '% is not a valid actor.org_unit shortname or ID; no change made.', 
1639                         new_owning_lib;
1640                 RETURN;
1641         END IF;
1642
1643 END;
1644
1645 $$ LANGUAGE plpgsql;
1646
1647 CREATE OR REPLACE FUNCTION migration_tools.marc_parses( TEXT ) RETURNS BOOLEAN AS $func$
1648
1649 use MARC::Record;
1650 use MARC::File::XML (BinaryEncoding => 'UTF-8');
1651 use MARC::Charset;
1652
1653 MARC::Charset->assume_unicode(1);
1654
1655 my $xml = shift;
1656
1657 eval {
1658     my $r = MARC::Record->new_from_xml( $xml );
1659     my $output_xml = $r->as_xml_record();
1660 };
1661 if ($@) {
1662     return 0;
1663 } else {
1664     return 1;
1665 }
1666
1667 $func$ LANGUAGE PLPERLU;
1668 COMMENT ON FUNCTION migration_tools.marc_parses(TEXT) IS 'Return boolean indicating if MARCXML string is parseable by MARC::File::XML';
1669
1670 CREATE OR REPLACE FUNCTION migration_tools.simple_export_library_config(dir TEXT, orgs INT[]) RETURNS VOID AS $FUNC$
1671 BEGIN
1672    EXECUTE $$COPY (SELECT * FROM actor.hours_of_operation WHERE id IN ($$ ||
1673            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1674            $$) TO '$$ ||  dir || $$/actor_hours_of_operation'$$;
1675    EXECUTE $$COPY (SELECT org_unit, close_start, close_end, reason FROM actor.org_unit_closed WHERE org_unit IN ($$ ||
1676            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1677            $$) TO '$$ ||  dir || $$/actor_org_unit_closed'$$;
1678    EXECUTE $$COPY (SELECT org_unit, name, value FROM actor.org_unit_setting WHERE org_unit IN ($$ ||
1679            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1680            $$) TO '$$ ||  dir || $$/actor_org_unit_setting'$$;
1681    EXECUTE $$COPY (SELECT name, owning_lib, holdable, hold_verify, opac_visible, circulate FROM asset.copy_location WHERE owning_lib IN ($$ ||
1682            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1683            $$) TO '$$ ||  dir || $$/asset_copy_location'$$;
1684    EXECUTE $$COPY (SELECT grp, org_unit, penalty, threshold FROM permission.grp_penalty_threshold WHERE org_unit IN ($$ ||
1685            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1686            $$) TO '$$ ||  dir || $$/permission_grp_penalty_threshold'$$;
1687    EXECUTE $$COPY (SELECT owning_lib, label, label_sortkey FROM asset.call_number_prefix WHERE owning_lib IN ($$ ||
1688            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1689            $$) TO '$$ ||  dir || $$/asset_call_number_prefix'$$;
1690    EXECUTE $$COPY (SELECT owning_lib, label, label_sortkey FROM asset.call_number_suffix WHERE owning_lib IN ($$ ||
1691            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1692            $$) TO '$$ ||  dir || $$/asset_call_number_suffix'$$;
1693    EXECUTE $$COPY config.rule_circ_duration TO '$$ ||  dir || $$/config_rule_circ_duration'$$;
1694    EXECUTE $$COPY config.rule_age_hold_protect TO '$$ ||  dir || $$/config_rule_age_hold_protect'$$;
1695    EXECUTE $$COPY config.rule_max_fine TO '$$ ||  dir || $$/config_rule_max_fine'$$;
1696    EXECUTE $$COPY config.rule_recurring_fine TO '$$ ||  dir || $$/config_rule_recurring_fine'$$;
1697    EXECUTE $$COPY permission.grp_tree TO '$$ ||  dir || $$/permission_grp_tree'$$;
1698 END;
1699 $FUNC$ LANGUAGE PLPGSQL;
1700
1701 CREATE OR REPLACE FUNCTION migration_tools.simple_import_library_config(dir TEXT) RETURNS VOID AS $FUNC$
1702 BEGIN
1703    EXECUTE $$COPY actor.hours_of_operation FROM '$$ ||  dir || $$/actor_hours_of_operation'$$;
1704    EXECUTE $$COPY actor.org_unit_closed (org_unit, close_start, close_end, reason) FROM '$$ ||  dir || $$/actor_org_unit_closed'$$;
1705    EXECUTE $$COPY actor.org_unit_setting (org_unit, name, value) FROM '$$ ||  dir || $$/actor_org_unit_setting'$$;
1706    EXECUTE $$COPY asset.copy_location (name, owning_lib, holdable, hold_verify, opac_visible, circulate) FROM '$$ ||  dir || $$/asset_copy_location'$$;
1707    EXECUTE $$COPY permission.grp_penalty_threshold (grp, org_unit, penalty, threshold) FROM '$$ ||  dir || $$/permission_grp_penalty_threshold'$$;
1708    EXECUTE $$COPY asset.call_number_prefix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/asset_call_number_prefix'$$;
1709    EXECUTE $$COPY asset.call_number_suffix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/asset_call_number_suffix'$$;
1710
1711    -- import any new circ rules
1712    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_circ_duration', 'id', 'name');
1713    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_age_hold_protect', 'id', 'name');
1714    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_max_fine', 'id', 'name');
1715    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_recurring_fine', 'id', 'name');
1716
1717    -- and permission groups
1718    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'permission', 'grp_tree', 'id', 'name');
1719
1720 END;
1721 $FUNC$ LANGUAGE PLPGSQL;
1722
1723
1724 CREATE OR REPLACE FUNCTION migration_tools.simple_import_new_rows_by_value(dir TEXT, schemaname TEXT, tablename TEXT, idcol TEXT, matchcol TEXT) RETURNS VOID AS $FUNC$
1725 DECLARE
1726     name TEXT;
1727     loopq TEXT;
1728     existsq TEXT;
1729     ct INTEGER;
1730     cols TEXT[];
1731     copyst TEXT;
1732 BEGIN
1733     EXECUTE $$DROP TABLE IF EXISTS tmp_$$ || tablename;
1734     EXECUTE $$CREATE TEMPORARY TABLE tmp_$$ || tablename || $$ AS SELECT * FROM $$ || schemaname || '.' || tablename || $$ LIMIT 0$$;
1735     EXECUTE $$COPY tmp_$$ || tablename || $$ FROM '$$ ||  dir || '/' || schemaname || '_' || tablename || $$'$$;
1736     loopq := 'SELECT ' || matchcol || ' FROM tmp_' || tablename || ' ORDER BY ' || idcol;
1737     existsq := 'SELECT COUNT(*) FROM ' || schemaname || '.' || tablename || ' WHERE ' || matchcol || ' = $1';
1738     SELECT ARRAY_AGG(column_name::TEXT) INTO cols FROM information_schema.columns WHERE table_schema = schemaname AND table_name = tablename AND column_name <> idcol;
1739     FOR name IN EXECUTE loopq LOOP
1740        EXECUTE existsq INTO ct USING name;
1741        IF ct = 0 THEN
1742            RAISE NOTICE 'inserting %.% row for %', schemaname, tablename, name;
1743            copyst := 'INSERT INTO ' || schemaname || '.' || tablename || ' (' || ARRAY_TO_STRING(cols, ',') || ') SELECT ' || ARRAY_TO_STRING(cols, ',') || 
1744                      ' FROM tmp_' || tablename || ' WHERE ' || matchcol || ' = $1';
1745            EXECUTE copyst USING name;
1746        END IF;
1747     END LOOP;
1748 END;
1749 $FUNC$ LANGUAGE PLPGSQL;
1750
1751 CREATE OR REPLACE FUNCTION migration_tools.merge_marc_fields( TEXT, TEXT, TEXT[] ) RETURNS TEXT AS $func$
1752
1753 use strict;
1754 use warnings;
1755
1756 use MARC::Record;
1757 use MARC::File::XML (BinaryEncoding => 'UTF-8');
1758 use MARC::Charset;
1759
1760 MARC::Charset->assume_unicode(1);
1761
1762 my $target_xml = shift;
1763 my $source_xml = shift;
1764 my $tags = shift;
1765
1766 my $target;
1767 my $source;
1768
1769 eval { $target = MARC::Record->new_from_xml( $target_xml ); };
1770 if ($@) {
1771     return;
1772 }
1773 eval { $source = MARC::Record->new_from_xml( $source_xml ); };
1774 if ($@) {
1775     return;
1776 }
1777
1778 my $source_id = $source->subfield('901', 'c');
1779 $source_id = $source->subfield('903', 'a') unless $source_id;
1780 my $target_id = $target->subfield('901', 'c');
1781 $target_id = $target->subfield('903', 'a') unless $target_id;
1782
1783 my %existing_fields;
1784 foreach my $tag (@$tags) {
1785     my %existing_fields = map { $_->as_formatted() => 1 } $target->field($tag);
1786     my @to_add = grep { not exists $existing_fields{$_->as_formatted()} } $source->field($tag);
1787     $target->insert_fields_ordered(map { $_->clone() } @to_add);
1788     if (@to_add) {
1789         elog(NOTICE, "Merged $tag tag(s) from $source_id to $target_id");
1790     }
1791 }
1792
1793 my $xml = $target->as_xml_record;
1794 $xml =~ s/^<\?.+?\?>$//mo;
1795 $xml =~ s/\n//sgo;
1796 $xml =~ s/>\s+</></sgo;
1797
1798 return $xml;
1799
1800 $func$ LANGUAGE PLPERLU;
1801 COMMENT ON FUNCTION migration_tools.merge_marc_fields( TEXT, TEXT, TEXT[] ) IS 'Given two MARCXML strings and an array of tags, returns MARCXML representing the merge of the specified fields from the second MARCXML record into the first.';
1802
1803 CREATE OR REPLACE FUNCTION migration_tools.make_stub_bib (text[], text[]) RETURNS TEXT AS $func$
1804
1805 use strict;
1806 use warnings;
1807
1808 use MARC::Record;
1809 use MARC::File::XML (BinaryEncoding => 'UTF-8');
1810 use Text::CSV;
1811
1812 my $in_tags = shift;
1813 my $in_values = shift;
1814
1815 # hack-and-slash parsing of array-passed-as-string;
1816 # this can go away once everybody is running Postgres 9.1+
1817 my $csv = Text::CSV->new({binary => 1});
1818 $in_tags =~ s/^{//;
1819 $in_tags =~ s/}$//;
1820 my $status = $csv->parse($in_tags);
1821 my $tags = [ $csv->fields() ];
1822 $in_values =~ s/^{//;
1823 $in_values =~ s/}$//;
1824 $status = $csv->parse($in_values);
1825 my $values = [ $csv->fields() ];
1826
1827 my $marc = MARC::Record->new();
1828
1829 $marc->leader('00000nam a22000007  4500');
1830 $marc->append_fields(MARC::Field->new('008', '000000s                       000   eng d'));
1831
1832 foreach my $i (0..$#$tags) {
1833     my ($tag, $sf);
1834     if ($tags->[$i] =~ /^(\d{3})([0-9a-z])$/) {
1835         $tag = $1;
1836         $sf = $2;
1837         $marc->append_fields(MARC::Field->new($tag, ' ', ' ', $sf => $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
1838     } elsif ($tags->[$i] =~ /^(\d{3})$/) {
1839         $tag = $1;
1840         $marc->append_fields(MARC::Field->new($tag, $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
1841     }
1842 }
1843
1844 my $xml = $marc->as_xml_record;
1845 $xml =~ s/^<\?.+?\?>$//mo;
1846 $xml =~ s/\n//sgo;
1847 $xml =~ s/>\s+</></sgo;
1848
1849 return $xml;
1850
1851 $func$ LANGUAGE PLPERLU;
1852 COMMENT ON FUNCTION migration_tools.make_stub_bib (text[], text[]) IS $$Simple function to create a stub MARCXML bib from a set of columns.
1853 The first argument is an array of tag/subfield specifiers, e.g., ARRAY['001', '245a', '500a'].
1854 The second argument is an array of text containing the values to plug into each field.  
1855 If the value for a given field is NULL or the empty string, it is not inserted.
1856 $$;
1857
1858 CREATE OR REPLACE FUNCTION migration_tools.set_indicator (TEXT, TEXT, INTEGER, CHAR(1)) RETURNS TEXT AS $func$
1859
1860 my ($marcxml, $tag, $pos, $value) = @_;
1861
1862 use MARC::Record;
1863 use MARC::File::XML (BinaryEncoding => 'UTF-8');
1864 use MARC::Charset;
1865 use strict;
1866
1867 MARC::Charset->assume_unicode(1);
1868
1869 elog(ERROR, 'indicator position must be either 1 or 2') unless $pos =~ /^[12]$/;
1870 elog(ERROR, 'MARC tag must be numeric') unless $tag =~ /^\d{3}$/;
1871 elog(ERROR, 'MARC tag must not be control field') if $tag =~ /^00/;
1872 elog(ERROR, 'Value must be exactly one character') unless $value =~ /^.$/;
1873
1874 my $xml = $marcxml;
1875 eval {
1876     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
1877
1878     foreach my $field ($marc->field($tag)) {
1879         $field->update("ind$pos" => $value);
1880     }
1881     $xml = $marc->as_xml_record;
1882     $xml =~ s/^<\?.+?\?>$//mo;
1883     $xml =~ s/\n//sgo;
1884     $xml =~ s/>\s+</></sgo;
1885 };
1886 return $xml;
1887
1888 $func$ LANGUAGE PLPERLU;
1889
1890 COMMENT ON FUNCTION migration_tools.set_indicator(TEXT, TEXT, INTEGER, CHAR(1)) IS $$Set indicator value of a specified MARC field.
1891 The first argument is a MARCXML string.
1892 The second argument is a MARC tag.
1893 The third argument is the indicator position, either 1 or 2.
1894 The fourth argument is the character to set the indicator value to.
1895 All occurences of the specified field will be changed.
1896 The function returns the revised MARCXML string.$$;
1897
1898 CREATE OR REPLACE FUNCTION migration_tools.create_staff_user(
1899     username TEXT,
1900     password TEXT,
1901     org TEXT,
1902     perm_group TEXT,
1903     first_name TEXT DEFAULT '',
1904     last_name TEXT DEFAULT ''
1905 ) RETURNS VOID AS $func$
1906 BEGIN
1907     RAISE NOTICE '%', org ;
1908     INSERT INTO actor.usr (usrname, passwd, ident_type, first_given_name, family_name, home_ou, profile)
1909     SELECT username, password, 1, first_name, last_name, aou.id, pgt.id
1910     FROM   actor.org_unit aou, permission.grp_tree pgt
1911     WHERE  aou.shortname = org
1912     AND    pgt.name = perm_group;
1913 END
1914 $func$
1915 LANGUAGE PLPGSQL;