48da8b6bc4758287d36db891d1c9facf4489b7ef
[migration-tools.git] / sql / base / base.sql
1 -- Copyright 2009-2012, Equinox Software, Inc.
2 --
3 -- This program is free software; you can redistribute it and/or
4 -- modify it under the terms of the GNU General Public License
5 -- as published by the Free Software Foundation; either version 2
6 -- of the License, or (at your option) any later version.
7 --
8 -- This program is distributed in the hope that it will be useful,
9 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
10 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 -- GNU General Public License for more details.
12 --
13 -- You should have received a copy of the GNU General Public License
14 -- along with this program; if not, write to the Free Software
15 -- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16
17 --------------------------------------------------------------------------
18 -- An example of how to use:
19 -- 
20 -- DROP SCHEMA foo CASCADE; CREATE SCHEMA foo; 
21 -- \i base.sql
22 -- SELECT migration_tools.init('foo');
23 -- SELECT migration_tools.build('foo');
24 -- SELECT * FROM foo.fields_requiring_mapping;
25 -- \d foo.actor_usr
26 -- create some incoming ILS specific staging tables, like CREATE foo.legacy_items ( l_barcode TEXT, .. ) INHERITS foo.asset_copy;
27 -- Do some mapping, like UPDATE foo.legacy_items SET barcode = TRIM(BOTH ' ' FROM l_barcode);
28 -- Then, to move into production, do: select migration_tools.insert_base_into_production('foo')
29
30 CREATE SCHEMA migration_tools;
31
32 CREATE OR REPLACE FUNCTION migration_tools.production_tables (TEXT) RETURNS TEXT[] AS $$
33     DECLARE
34         migration_schema ALIAS FOR $1;
35         output  RECORD;
36     BEGIN
37         FOR output IN
38             EXECUTE 'SELECT string_to_array(value,'','') AS tables FROM ' || migration_schema || '.config WHERE key = ''production_tables'';'
39         LOOP
40             RETURN output.tables;
41         END LOOP;
42     END;
43 $$ LANGUAGE PLPGSQL STRICT STABLE;
44
45 CREATE OR REPLACE FUNCTION migration_tools.country_code (TEXT) RETURNS TEXT AS $$
46     DECLARE
47         migration_schema ALIAS FOR $1;
48         output TEXT;
49     BEGIN
50         FOR output IN
51             EXECUTE 'SELECT value FROM ' || migration_schema || '.config WHERE key = ''country_code'';'
52         LOOP
53             RETURN output;
54         END LOOP;
55     END;
56 $$ LANGUAGE PLPGSQL STRICT STABLE;
57
58
59 CREATE OR REPLACE FUNCTION migration_tools.log (TEXT,TEXT,INTEGER) RETURNS VOID AS $$
60     DECLARE
61         migration_schema ALIAS FOR $1;
62         sql ALIAS FOR $2;
63         nrows ALIAS FOR $3;
64     BEGIN
65         EXECUTE 'INSERT INTO ' || migration_schema || '.sql_log ( sql, row_count ) VALUES ( ' || quote_literal(sql) || ', ' || nrows || ' );';
66     END;
67 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
68
69 CREATE OR REPLACE FUNCTION migration_tools.exec (TEXT,TEXT) RETURNS VOID AS $$
70     DECLARE
71         migration_schema ALIAS FOR $1;
72         sql ALIAS FOR $2;
73         nrows INTEGER;
74     BEGIN
75         EXECUTE 'UPDATE ' || migration_schema || '.sql_current SET sql = ' || quote_literal(sql) || ';';
76         --RAISE INFO '%', sql;
77         EXECUTE sql;
78         GET DIAGNOSTICS nrows = ROW_COUNT;
79         PERFORM migration_tools.log(migration_schema,sql,nrows);
80     EXCEPTION
81         WHEN OTHERS THEN 
82             RAISE EXCEPTION '!!!!!!!!!!! state = %, msg = %, sql = %', SQLSTATE, SQLERRM, sql;
83     END;
84 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
85
86 CREATE OR REPLACE FUNCTION migration_tools.debug_exec (TEXT,TEXT) RETURNS VOID AS $$
87     DECLARE
88         migration_schema ALIAS FOR $1;
89         sql ALIAS FOR $2;
90         nrows INTEGER;
91     BEGIN
92         EXECUTE 'UPDATE ' || migration_schema || '.sql_current SET sql = ' || quote_literal(sql) || ';';
93         RAISE INFO 'debug_exec sql = %', sql;
94         EXECUTE sql;
95         GET DIAGNOSTICS nrows = ROW_COUNT;
96         PERFORM migration_tools.log(migration_schema,sql,nrows);
97     EXCEPTION
98         WHEN OTHERS THEN 
99             RAISE EXCEPTION '!!!!!!!!!!! state = %, msg = %, sql = %', SQLSTATE, SQLERRM, sql;
100     END;
101 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
102
103 CREATE OR REPLACE FUNCTION migration_tools.init (TEXT) RETURNS VOID AS $$
104     DECLARE
105         migration_schema ALIAS FOR $1;
106         sql TEXT;
107     BEGIN
108         EXECUTE 'DROP TABLE IF EXISTS ' || migration_schema || '.sql_current;';
109         EXECUTE 'CREATE TABLE ' || migration_schema || '.sql_current ( sql TEXT);';
110         EXECUTE 'INSERT INTO ' || migration_schema || '.sql_current ( sql ) VALUES ( '''' );';
111         BEGIN
112             SELECT 'CREATE TABLE ' || migration_schema || '.sql_log ( time TIMESTAMP NOT NULL DEFAULT NOW(), row_count INTEGER, sql TEXT );' INTO STRICT sql;
113             EXECUTE sql;
114         EXCEPTION
115             WHEN OTHERS THEN 
116                 RAISE INFO '!!!!!!!!!!! state = %, msg = %, sql = %', SQLSTATE, SQLERRM, sql;
117         END;
118         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.config;' );
119         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.config ( key TEXT UNIQUE, value TEXT);' );
120         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''production_tables'', ''asset.call_number,asset.call_number_prefix,asset.call_number_suffix,asset.copy_location,asset.copy,asset.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,action.hold_request_note,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,config.circ_matrix_matchpoint,config.circ_matrix_limit_set_map,config.hold_matrix_matchpoint'' );' );
121         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''country_code'', ''USA'' );' );
122         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.fields_requiring_mapping;' );
123         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.fields_requiring_mapping( table_schema TEXT, table_name TEXT, column_name TEXT, data_type TEXT);' );
124         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_profile_map;' );  
125         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_profile_map ( 
126             id SERIAL,
127             perm_grp_id INTEGER,
128             transcribed_perm_group TEXT,
129             legacy_field1 TEXT,
130             legacy_value1 TEXT,
131             legacy_field2 TEXT,
132             legacy_value2 TEXT,
133             legacy_field3 TEXT,
134             legacy_value3 TEXT
135         );' );
136         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_profile_map'', ''base_profile_map'' );' );
137         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_item_dynamic_field_map;' );  
138         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_item_dynamic_field_map ( 
139             id SERIAL,
140             evergreen_field TEXT,
141             evergreen_value TEXT,
142             evergreen_datatype TEXT,
143             legacy_field1 TEXT,
144             legacy_value1 TEXT,
145             legacy_field2 TEXT,
146             legacy_value2 TEXT,
147             legacy_field3 TEXT,
148             legacy_value3 TEXT
149         );' );
150         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_item_dynamic_lf1_idx ON ' || migration_schema || '.base_item_dynamic_field_map (legacy_field1,legacy_value1);' ); 
151         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_item_dynamic_lf2_idx ON ' || migration_schema || '.base_item_dynamic_field_map (legacy_field2,legacy_value2);' ); 
152         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_item_dynamic_lf3_idx ON ' || migration_schema || '.base_item_dynamic_field_map (legacy_field3,legacy_value3);' ); 
153         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_item_dynamic_field_map'', ''base_item_dynamic_field_map'' );' );
154         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_copy_location_map;' );  
155         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_copy_location_map ( 
156             id SERIAL,
157             location INTEGER,
158             holdable BOOLEAN NOT NULL DEFAULT TRUE,
159             hold_verify BOOLEAN NOT NULL DEFAULT FALSE,
160             opac_visible BOOLEAN NOT NULL DEFAULT TRUE,
161             circulate BOOLEAN NOT NULL DEFAULT TRUE,
162             transcribed_location TEXT,
163             legacy_field1 TEXT,
164             legacy_value1 TEXT,
165             legacy_field2 TEXT,
166             legacy_value2 TEXT,
167             legacy_field3 TEXT,
168             legacy_value3 TEXT
169         );' );
170         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_base_copy_location_lf1_idx ON ' || migration_schema || '.base_copy_location_map (legacy_field1,legacy_value1);' ); 
171         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_base_copy_location_lf2_idx ON ' || migration_schema || '.base_copy_location_map (legacy_field2,legacy_value2);' ); 
172         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_base_copy_location_lf3_idx ON ' || migration_schema || '.base_copy_location_map (legacy_field3,legacy_value3);' ); 
173         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_base_copy_location_loc_idx ON ' || migration_schema || '.base_copy_location_map (transcribed_location);' ); 
174         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_copy_location_map'', ''base_copy_location_map'' );' );
175         PERFORM migration_tools.exec( $1, 'DROP TABLE IF EXISTS ' || migration_schema || '.base_circ_field_map;' );  
176         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || E'.base_circ_field_map ( 
177             id SERIAL,
178             circulate BOOLEAN,
179             loan_period TEXT,
180             max_renewals TEXT,
181             max_out TEXT,
182             fine_amount TEXT,
183             fine_interval TEXT,
184             max_fine TEXT,
185             item_field1 TEXT,
186             item_value1 TEXT,
187             item_field2 TEXT,
188             item_value2 TEXT,
189             patron_field1 TEXT,
190             patron_value1 TEXT,
191             patron_field2 TEXT,
192             patron_value2 TEXT
193         );' );
194         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf1_idx ON ' || migration_schema || '.base_circ_field_map (item_field1,item_value1);' ); 
195         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf2_idx ON ' || migration_schema || '.base_circ_field_map (item_field2,item_value2);' ); 
196         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf3_idx ON ' || migration_schema || '.base_circ_field_map (patron_field1,patron_value1);' ); 
197         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_circ_dynamic_lf4_idx ON ' || migration_schema || '.base_circ_field_map (patron_field2,patron_value2);' ); 
198         PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''base_circ_field_map'', ''base_circ_field_map'' );' );
199
200         BEGIN
201             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_init'', now() );' );
202         EXCEPTION
203             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_init'';' );
204         END;
205     END;
206 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
207
208 CREATE OR REPLACE FUNCTION migration_tools.build (TEXT) RETURNS VOID AS $$
209     DECLARE
210         migration_schema ALIAS FOR $1;
211         production_tables TEXT[];
212     BEGIN
213         --RAISE INFO 'In migration_tools.build(%)', migration_schema;
214         SELECT migration_tools.production_tables(migration_schema) INTO STRICT production_tables;
215         PERFORM migration_tools.build_base_staging_tables(migration_schema,production_tables);
216         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_patron_barcode_key ON ' || migration_schema || '.actor_card ( barcode );' );
217         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_patron_usrname_key ON ' || migration_schema || '.actor_usr ( usrname );' );
218         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_copy_barcode_key ON ' || migration_schema || '.asset_copy ( barcode );' );
219         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_copy_id_key ON ' || migration_schema || '.asset_copy ( id );' );
220         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_callnum_record_idx ON ' || migration_schema || '.asset_call_number ( record );' );
221         PERFORM migration_tools.exec( $1, 'CREATE INDEX ' || migration_schema || '_callnum_upper_label_id_lib_idx ON ' || migration_schema || '.asset_call_number ( UPPER(label),id,owning_lib );' );
222         PERFORM migration_tools.exec( $1, 'CREATE UNIQUE INDEX ' || migration_schema || '_callnum_label_once_per_lib ON ' || migration_schema || '.asset_call_number ( record,owning_lib,label,prefix,suffix );' );
223     END;
224 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
225
226 CREATE OR REPLACE FUNCTION migration_tools.build_base_staging_tables (TEXT,TEXT[]) RETURNS VOID AS $$
227     DECLARE
228         migration_schema ALIAS FOR $1;
229         production_tables ALIAS FOR $2;
230     BEGIN
231         --RAISE INFO 'In migration_tools.build_base_staging_tables(%,%)', migration_schema, production_tables;
232         FOR i IN array_lower(production_tables,1) .. array_upper(production_tables,1) LOOP
233             PERFORM migration_tools.build_specific_base_staging_table(migration_schema,production_tables[i]);
234         END LOOP;
235     END;
236 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
237
238 CREATE OR REPLACE FUNCTION migration_tools.build_specific_base_staging_table (TEXT,TEXT) RETURNS VOID AS $$
239     DECLARE
240         migration_schema ALIAS FOR $1;
241         production_table ALIAS FOR $2;
242         base_staging_table TEXT;
243         columns RECORD;
244     BEGIN
245         base_staging_table = REPLACE( production_table, '.', '_' );
246         --RAISE INFO 'In migration_tools.build_specific_base_staging_table(%,%) -> %', migration_schema, production_table, base_staging_table;
247         PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.' || base_staging_table || ' ( LIKE ' || production_table || ' INCLUDING DEFAULTS EXCLUDING CONSTRAINTS );' );
248         PERFORM migration_tools.exec( $1, '
249             INSERT INTO ' || migration_schema || '.fields_requiring_mapping
250                 SELECT table_schema, table_name, column_name, data_type
251                 FROM information_schema.columns 
252                 WHERE table_schema = ''' || migration_schema || ''' AND table_name = ''' || base_staging_table || ''' AND is_nullable = ''NO'' AND column_default IS NULL;
253         ' );
254         FOR columns IN 
255             SELECT table_schema, table_name, column_name, data_type
256             FROM information_schema.columns 
257             WHERE table_schema = migration_schema AND table_name = base_staging_table AND is_nullable = 'NO' AND column_default IS NULL
258         LOOP
259             PERFORM migration_tools.exec( $1, 'ALTER TABLE ' || columns.table_schema || '.' || columns.table_name || ' ALTER COLUMN ' || columns.column_name || ' DROP NOT NULL;' );
260         END LOOP;
261     END;
262 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
263
264 CREATE OR REPLACE FUNCTION migration_tools.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.name_parse_out_last_first_middle_and_random_affix (TEXT) RETURNS TEXT[] AS $$
356     DECLARE
357         full_name TEXT := $1;
358         temp TEXT;
359         family_name TEXT := '';
360         first_given_name TEXT := '';
361         second_given_name TEXT := '';
362         suffix TEXT := '';
363         prefix TEXT := '';
364     BEGIN
365         temp := full_name;
366         -- Use values, not structure, for prefix/suffix, unless we come up with a better idea
367         IF temp ilike '%MR.%' THEN
368             prefix := 'Mr.';
369             temp := REGEXP_REPLACE( temp, E'MR\.\\s*', '', 'i' );
370         END IF;
371         IF temp ilike '%MRS.%' THEN
372             prefix := 'Mrs.';
373             temp := REGEXP_REPLACE( temp, E'MRS\.\\s*', '', 'i' );
374         END IF;
375         IF temp ilike '%MS.%' THEN
376             prefix := 'Ms.';
377             temp := REGEXP_REPLACE( temp, E'MS\.\\s*', '', 'i' );
378         END IF;
379         IF temp ilike '%DR.%' THEN
380             prefix := 'Dr.';
381             temp := REGEXP_REPLACE( temp, E'DR\.\\s*', '', 'i' );
382         END IF;
383         IF temp ilike '%JR%' THEN
384             suffix := 'Jr.';
385             temp := REGEXP_REPLACE( temp, E'JR\.?\\s*', '', 'i' );
386         END IF;
387         IF temp ilike '%JR,%' THEN
388             suffix := 'Jr.';
389             temp := REGEXP_REPLACE( temp, E'JR,\\s*', ',', 'i' );
390         END IF;
391         IF temp ilike '%SR%' THEN
392             suffix := 'Sr.';
393             temp := REGEXP_REPLACE( temp, E'SR\.?\\s*', '', 'i' );
394         END IF;
395         IF temp ilike '%SR,%' THEN
396             suffix := 'Sr.';
397             temp := REGEXP_REPLACE( temp, E'SR,\\s*', ',', 'i' );
398         END IF;
399         IF temp ilike '%III%' THEN
400             suffix := 'III';
401             temp := REGEXP_REPLACE( temp, E'III', '', 'i' );
402         END IF;
403         IF temp ilike '%II%' THEN
404             suffix := 'II';
405             temp := REGEXP_REPLACE( temp, E'II', '', 'i' );
406         END IF;
407         IF temp ilike '%IV%' THEN
408             suffix := 'IV';
409             temp := REGEXP_REPLACE( temp, E'IV', '', 'i' );
410         END IF;
411
412         family_name := BTRIM( REGEXP_REPLACE(temp,E'^(\\S+).*$',E'\\1') );
413         first_given_name := CASE WHEN temp ~ E'^\\S+$' THEN 'N/A' ELSE BTRIM( REGEXP_REPLACE(temp,E'^\\S+\\s+(\\S+).*$',E'\\1') ) END;
414         second_given_name := CASE WHEN temp ~ E'^\\S+$' THEN '' ELSE BTRIM( REGEXP_REPLACE(temp,E'^\\S+\\s+\\S+\\s*(.*)$',E'\\1') ) END;
415
416         RETURN ARRAY[ family_name, prefix, first_given_name, second_given_name, suffix ];
417     END;
418 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
419
420
421 CREATE OR REPLACE FUNCTION migration_tools.address_parse_out_citystatezip (TEXT) RETURNS TEXT[] AS $$
422     DECLARE
423         city_state_zip TEXT := $1;
424         city TEXT := '';
425         state TEXT := '';
426         zip TEXT := '';
427     BEGIN
428         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;
429         city_state_zip := REGEXP_REPLACE( city_state_zip, E'^(.*)\\d\\d\\d\\d\\d-?\\d*(.*)$', E'\\1\\2');
430         IF city_state_zip ~ ',' THEN
431             state := REGEXP_REPLACE( city_state_zip, E'^(.*),(.*)$', E'\\2');
432             city := REGEXP_REPLACE( city_state_zip, E'^(.*),(.*)$', E'\\1');
433         ELSE
434             IF city_state_zip ~ E'\\s+[A-Z][A-Z]\\s*$' THEN
435                 state := REGEXP_REPLACE( city_state_zip, E'^.*,?\\s+([A-Z][A-Z])\\s*$', E'\\1' );
436                 city := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s+[A-Z][A-Z](\\s*)$', E'\\1\\2' );
437             ELSE
438                 IF city_state_zip ~ E'^\\S+$'  THEN
439                     city := city_state_zip;
440                     state := 'N/A';
441                 ELSE
442                     state := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\2');
443                     city := REGEXP_REPLACE( city_state_zip, E'^(.*?),?\\s*(\\S+)\\s*$', E'\\1');
444                 END IF;
445             END IF;
446         END IF;
447         RETURN ARRAY[ TRIM(BOTH ' ' FROM city), TRIM(BOTH ' ' FROM state), TRIM(BOTH ' ' FROM zip) ];
448     END;
449 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
450
451 CREATE OR REPLACE FUNCTION migration_tools.rebarcode (o TEXT, t BIGINT) RETURNS TEXT AS $$
452     DECLARE
453         n TEXT := o;
454     BEGIN
455         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
456             IF o::BIGINT < t THEN
457                 n = o::BIGINT + t;
458             END IF;
459         END IF;
460
461         RETURN n;
462     END;
463 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
464
465 CREATE OR REPLACE FUNCTION migration_tools.base_profile_map (TEXT) RETURNS TEXT AS $$
466     DECLARE
467         migration_schema ALIAS FOR $1;
468         output TEXT;
469     BEGIN
470         FOR output IN
471             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_profile_map'';'
472         LOOP
473             RETURN output;
474         END LOOP;
475     END;
476 $$ LANGUAGE PLPGSQL STRICT STABLE;
477
478 CREATE OR REPLACE FUNCTION migration_tools.base_item_dynamic_field_map (TEXT) RETURNS TEXT AS $$
479     DECLARE
480         migration_schema ALIAS FOR $1;
481         output TEXT;
482     BEGIN
483         FOR output IN
484             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_item_dynamic_field_map'';'
485         LOOP
486             RETURN output;
487         END LOOP;
488     END;
489 $$ LANGUAGE PLPGSQL STRICT STABLE;
490
491 CREATE OR REPLACE FUNCTION migration_tools.base_copy_location_map (TEXT) RETURNS TEXT AS $$
492     DECLARE
493         migration_schema ALIAS FOR $1;
494         output TEXT;
495     BEGIN
496         FOR output IN
497             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_copy_location_map'';'
498         LOOP
499             RETURN output;
500         END LOOP;
501     END;
502 $$ LANGUAGE PLPGSQL STRICT STABLE;
503
504 CREATE OR REPLACE FUNCTION migration_tools.base_circ_field_map (TEXT) RETURNS TEXT AS $$
505     DECLARE
506         migration_schema ALIAS FOR $1;
507         output TEXT;
508     BEGIN
509         FOR output IN
510             EXECUTE 'SELECT ''' || migration_schema || '.'' || value FROM ' || migration_schema || '.config WHERE key = ''base_circ_field_map'';'
511         LOOP
512             RETURN output;
513         END LOOP;
514     END;
515 $$ LANGUAGE PLPGSQL STRICT STABLE;
516
517 CREATE OR REPLACE FUNCTION migration_tools.map_base_patron_profile (TEXT,TEXT,INTEGER) RETURNS VOID AS $$
518     DECLARE
519         migration_schema ALIAS FOR $1;
520         profile_map TEXT;
521         patron_table ALIAS FOR $2;
522         default_patron_profile ALIAS FOR $3;
523         sql TEXT;
524         sql_update TEXT;
525         sql_where1 TEXT := '';
526         sql_where2 TEXT := '';
527         sql_where3 TEXT := '';
528         output RECORD;
529     BEGIN
530         SELECT migration_tools.base_profile_map(migration_schema) INTO STRICT profile_map;
531         FOR output IN 
532             EXECUTE 'SELECT * FROM ' || profile_map || E' ORDER BY id;'
533         LOOP
534             sql_update := 'UPDATE ' || patron_table || ' AS u SET profile = perm_grp_id FROM ' || profile_map || ' AS m WHERE ';
535             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);
536             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);
537             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);
538             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,'') || ';';
539             --RAISE INFO 'sql = %', sql;
540             PERFORM migration_tools.exec( $1, sql );
541         END LOOP;
542         PERFORM migration_tools.exec( $1, 'UPDATE ' || patron_table || ' AS u SET profile = ' || quote_literal(default_patron_profile) || ' WHERE profile IS NULL;'  );
543         BEGIN
544             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_patron_mapping_profile'', now() );' );
545         EXCEPTION
546             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_patron_mapping_profile'';' );
547         END;
548     END;
549 $$ LANGUAGE PLPGSQL STRICT STABLE;
550
551 CREATE OR REPLACE FUNCTION migration_tools.map_base_item_table_dynamic (TEXT,TEXT) RETURNS VOID AS $$
552     DECLARE
553         migration_schema ALIAS FOR $1;
554         field_map TEXT;
555         item_table ALIAS FOR $2;
556         sql TEXT;
557         sql_update TEXT;
558         sql_where1 TEXT := '';
559         sql_where2 TEXT := '';
560         sql_where3 TEXT := '';
561         output RECORD;
562     BEGIN
563         SELECT migration_tools.base_item_dynamic_field_map(migration_schema) INTO STRICT field_map;
564         FOR output IN 
565             EXECUTE 'SELECT * FROM ' || field_map || E' ORDER BY id;'
566         LOOP
567             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 ';
568             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);
569             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);
570             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);
571             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,'') || ';';
572             --RAISE INFO 'sql = %', sql;
573             PERFORM migration_tools.exec( $1, sql );
574         END LOOP;
575         BEGIN
576             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_item_mapping_dynamic'', now() );' );
577         EXCEPTION
578             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_item_mapping_dynamic'';' );
579         END;
580     END;
581 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
582
583 CREATE OR REPLACE FUNCTION migration_tools.map_base_item_table_locations (TEXT,TEXT) RETURNS VOID AS $$
584     DECLARE
585         migration_schema ALIAS FOR $1;
586         base_copy_location_map TEXT;
587         item_table ALIAS FOR $2;
588         sql TEXT;
589         sql_update TEXT;
590         sql_where1 TEXT := '';
591         sql_where2 TEXT := '';
592         sql_where3 TEXT := '';
593         output RECORD;
594     BEGIN
595         SELECT migration_tools.base_copy_location_map(migration_schema) INTO STRICT base_copy_location_map;
596         FOR output IN 
597             EXECUTE 'SELECT * FROM ' || base_copy_location_map || E' ORDER BY id;'
598         LOOP
599             sql_update := 'UPDATE ' || item_table || ' AS i SET location = m.location FROM ' || base_copy_location_map || ' AS m WHERE ';
600             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);
601             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);
602             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);
603             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,'') || ';';
604             --RAISE INFO 'sql = %', sql;
605             PERFORM migration_tools.exec( $1, sql );
606         END LOOP;
607         BEGIN
608             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_item_mapping_locations'', now() );' );
609         EXCEPTION
610             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_item_mapping_locations'';' );
611         END;
612     END;
613 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
614
615 -- 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
616 CREATE OR REPLACE FUNCTION migration_tools.map_base_circ_table_dynamic (TEXT,TEXT,TEXT,TEXT) RETURNS VOID AS $$
617     DECLARE
618         migration_schema ALIAS FOR $1;
619         field_map TEXT;
620         circ_table ALIAS FOR $2;
621         item_table ALIAS FOR $3;
622         patron_table ALIAS FOR $4;
623         sql TEXT;
624         sql_update TEXT;
625         sql_where1 TEXT := '';
626         sql_where2 TEXT := '';
627         sql_where3 TEXT := '';
628         sql_where4 TEXT := '';
629         output RECORD;
630     BEGIN
631         SELECT migration_tools.base_circ_field_map(migration_schema) INTO STRICT field_map;
632         FOR output IN 
633             EXECUTE 'SELECT * FROM ' || field_map || E' ORDER BY id;'
634         LOOP
635             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 ';
636             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);
637             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);
638             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);
639             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);
640             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,'') || ';';
641             --RAISE INFO 'sql = %', sql;
642             PERFORM migration_tools.exec( $1, sql );
643         END LOOP;
644         BEGIN
645             PERFORM migration_tools.exec( $1, 'INSERT INTO ' || migration_schema || '.config (key,value) VALUES ( ''last_base_circ_field_mapping'', now() );' );
646         EXCEPTION
647             WHEN OTHERS THEN PERFORM migration_tools.exec( $1, 'UPDATE ' || migration_schema || '.config SET value = now() WHERE key = ''last_base_circ_field_mapping'';' );
648         END;
649     END;
650 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
651
652 -- expand_barcode
653 --   $barcode      source barcode
654 --   $prefix       prefix to add to barcode, NULL = add no prefix
655 --   $maxlen       maximum length of barcode; default to 14 if left NULL
656 --   $pad          padding string to apply to left of source barcode before adding
657 --                 prefix and suffix; set to NULL or '' if no padding is desired
658 --   $suffix       suffix to add to barcode, NULL = add no suffix
659 --
660 -- Returns a new string consisting of prefix concatenated with padded barcode and suffix.
661 -- If new barcode would be longer than $maxlen, the original barcode is returned instead.
662 --
663 CREATE OR REPLACE FUNCTION migration_tools.expand_barcode (TEXT, TEXT, INTEGER, TEXT, TEXT) RETURNS TEXT AS $$
664     my ($barcode, $prefix, $maxlen, $pad, $suffix) = @_;
665
666     # default case
667     return unless defined $barcode;
668
669     $prefix     = '' unless defined $prefix;
670     $maxlen ||= 14;
671     $pad        = '0' unless defined $pad;
672     $suffix     = '' unless defined $suffix;
673
674     # bail out if adding prefix and suffix would bring new barcode over max length
675     return $barcode if (length($prefix) + length($barcode) + length($suffix)) > $maxlen;
676
677     my $new_barcode = $barcode;
678     if ($pad ne '') {
679         my $pad_length = $maxlen - length($prefix) - length($suffix);
680         if (length($barcode) < $pad_length) {
681             # assuming we always want padding on the left
682             # also assuming that it is possible to have the pad string be longer than 1 character
683             $new_barcode = substr($pad x ($pad_length - length($barcode)), 0, $pad_length - length($barcode)) . $new_barcode;
684         }
685     }
686
687     # bail out if adding prefix and suffix would bring new barcode over max length
688     return $barcode if (length($prefix) + length($new_barcode) + length($suffix)) > $maxlen;
689
690     return "$prefix$new_barcode$suffix";
691 $$ LANGUAGE PLPERLU STABLE;
692
693 -- remove previous version of this function
694 DROP FUNCTION IF EXISTS migration_tools.attempt_cast(TEXT, TEXT, TEXT);
695
696 CREATE OR REPLACE FUNCTION migration_tools.attempt_cast (TEXT, TEXT) RETURNS TEXT AS $$
697     DECLARE
698         attempt_value ALIAS FOR $1;
699         datatype ALIAS FOR $2;
700     BEGIN
701         EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::' || datatype || ' AS a;';
702         RETURN attempt_value;
703     EXCEPTION
704         WHEN OTHERS THEN RETURN NULL;
705     END;
706 $$ LANGUAGE PLPGSQL STRICT STABLE;
707
708 CREATE OR REPLACE FUNCTION migration_tools.attempt_date (TEXT,TEXT) RETURNS DATE AS $$
709     DECLARE
710         attempt_value ALIAS FOR $1;
711         fail_value ALIAS FOR $2;
712         output DATE;
713     BEGIN
714         FOR output IN
715             EXECUTE 'SELECT ' || quote_literal(REGEXP_REPLACE(attempt_value,'^(\d\d)(\d\d)(\d\d)$','\1-\2-\3')) || '::date AS a;'
716         LOOP
717             RETURN output;
718         END LOOP;
719     EXCEPTION
720         WHEN OTHERS THEN
721             FOR output IN
722                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::date AS a;'
723             LOOP
724                 RETURN output;
725             END LOOP;
726     END;
727 $$ LANGUAGE PLPGSQL STRICT STABLE;
728
729 CREATE OR REPLACE FUNCTION migration_tools.attempt_timestamptz (TEXT,TEXT) RETURNS TIMESTAMPTZ AS $$
730     DECLARE
731         attempt_value ALIAS FOR $1;
732         fail_value ALIAS FOR $2;
733         output TIMESTAMPTZ;
734     BEGIN
735         FOR output IN
736             EXECUTE 'SELECT ' || quote_literal(attempt_value) || '::TIMESTAMPTZ AS a;'
737         LOOP
738             RETURN output;
739         END LOOP;
740     EXCEPTION
741         WHEN OTHERS THEN
742             FOR output IN
743                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::TIMESTAMPTZ AS a;'
744             LOOP
745                 RETURN output;
746             END LOOP;
747     END;
748 $$ LANGUAGE PLPGSQL STRICT STABLE;
749
750 CREATE OR REPLACE FUNCTION migration_tools.attempt_hz_date (TEXT,TEXT) RETURNS DATE AS $$
751     DECLARE
752         attempt_value ALIAS FOR $1;
753         fail_value ALIAS FOR $2;
754         output DATE;
755     BEGIN
756         FOR output IN
757             EXECUTE E'SELECT (\'1970-01-01\'::date + \'' || attempt_value || E' days\'::interval)::date AS a;'
758         LOOP
759             RETURN output;
760         END LOOP;
761     EXCEPTION
762         WHEN OTHERS THEN
763             FOR output IN
764                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::date AS a;'
765             LOOP
766                 RETURN output;
767             END LOOP;
768     END;
769 $$ LANGUAGE PLPGSQL STRICT STABLE;
770
771 CREATE OR REPLACE FUNCTION migration_tools.attempt_money (TEXT,TEXT) RETURNS NUMERIC(8,2) AS $$
772     DECLARE
773         attempt_value ALIAS FOR $1;
774         fail_value ALIAS FOR $2;
775         output NUMERIC(8,2);
776     BEGIN
777         FOR output IN
778             EXECUTE 'SELECT ' || quote_literal(REPLACE(REPLACE(attempt_value,'$',''),',','')) || '::NUMERIC(8,2) AS a;'
779         LOOP
780             RETURN output;
781         END LOOP;
782     EXCEPTION
783         WHEN OTHERS THEN
784             FOR output IN
785                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(8,2) AS a;'
786             LOOP
787                 RETURN output;
788             END LOOP;
789     END;
790 $$ LANGUAGE PLPGSQL STRICT STABLE;
791
792 CREATE OR REPLACE FUNCTION migration_tools.attempt_money6 (TEXT,TEXT) RETURNS NUMERIC(6,2) AS $$
793     DECLARE
794         attempt_value ALIAS FOR $1;
795         fail_value ALIAS FOR $2;
796         output NUMERIC(6,2);
797     BEGIN
798         FOR output IN
799             EXECUTE 'SELECT ' || quote_literal(REPLACE(REPLACE(attempt_value,'$',''),',','')) || '::NUMERIC(6,2) AS a;'
800         LOOP
801             RETURN output;
802         END LOOP;
803     EXCEPTION
804         WHEN OTHERS THEN
805             FOR output IN
806                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(6,2) AS a;'
807             LOOP
808                 RETURN output;
809             END LOOP;
810     END;
811 $$ LANGUAGE PLPGSQL STRICT STABLE;
812
813 CREATE OR REPLACE FUNCTION migration_tools.attempt_money_from_pennies (TEXT,TEXT) RETURNS NUMERIC(8,2) AS $$
814     DECLARE
815         attempt_value ALIAS FOR $1;
816         fail_value ALIAS FOR $2;
817         output NUMERIC(8,2);
818     BEGIN
819         IF length(regexp_replace(attempt_value,'^0+','')) > 10 THEN
820             RAISE EXCEPTION 'too many digits';
821         END IF;
822         FOR output IN
823             EXECUTE 'SELECT ' || quote_literal((left(lpad(regexp_replace(attempt_value,'^0+',''),10,'0'),-2) || '.' || right(lpad(regexp_replace(attempt_value,'^0+',''),10,'0'),2))::numeric(8,2)) || '::NUMERIC(8,2) AS a;'
824         LOOP
825             RETURN output;
826         END LOOP;
827     EXCEPTION
828         WHEN OTHERS THEN
829             FOR output IN
830                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(8,2) AS a;'
831             LOOP
832                 RETURN output;
833             END LOOP;
834     END;
835 $$ LANGUAGE PLPGSQL STRICT STABLE;
836
837 CREATE OR REPLACE FUNCTION migration_tools.attempt_money_from_pennies6 (TEXT,TEXT) RETURNS NUMERIC(6,2) AS $$
838     DECLARE
839         attempt_value ALIAS FOR $1;
840         fail_value ALIAS FOR $2;
841         output NUMERIC(6,2);
842     BEGIN
843         IF length(regexp_replace(attempt_value,'^0+','')) > 8 THEN
844             RAISE EXCEPTION 'too many digits';
845         END IF;
846         FOR output IN
847             EXECUTE 'SELECT ' || quote_literal((left(lpad(regexp_replace(attempt_value,'^0+',''),8,'0'),-2) || '.' || right(lpad(regexp_replace(attempt_value,'^0+',''),8,'0'),2))::numeric(6,2)) || '::NUMERIC(6,2) AS a;'
848         LOOP
849             RETURN output;
850         END LOOP;
851     EXCEPTION
852         WHEN OTHERS THEN
853             FOR output IN
854                 EXECUTE 'SELECT ' || quote_literal(fail_value) || '::NUMERIC(6,2) AS a;'
855             LOOP
856                 RETURN output;
857             END LOOP;
858     END;
859 $$ LANGUAGE PLPGSQL STRICT STABLE;
860
861 -- add_codabar_checkdigit
862 --   $barcode      source barcode
863 --
864 -- If the source string is 13 or 14 characters long and contains only digits, adds or replaces the 14
865 -- character with a checkdigit computed according to the usual algorithm for library barcodes
866 -- using the Codabar symbology - see <http://www.makebarcode.com/specs/codabar.html>.  If the
867 -- input string does not meet those requirements, it is returned unchanged.
868 --
869 CREATE OR REPLACE FUNCTION migration_tools.add_codabar_checkdigit (TEXT) RETURNS TEXT AS $$
870     my $barcode = shift;
871
872     return $barcode if $barcode !~ /^\d{13,14}$/;
873     $barcode = substr($barcode, 0, 13); # ignore 14th digit
874     my @digits = split //, $barcode;
875     my $total = 0;
876     $total += $digits[$_] foreach (1, 3, 5, 7, 9, 11);
877     $total += (2 * $digits[$_] >= 10) ? (2 * $digits[$_] - 9) : (2 * $digits[$_]) foreach (0, 2, 4, 6, 8, 10, 12);
878     my $remainder = $total % 10;
879     my $checkdigit = ($remainder == 0) ? $remainder : 10 - $remainder;
880     return $barcode . $checkdigit; 
881 $$ LANGUAGE PLPERLU STRICT STABLE;
882
883 -- add_code39mod43_checkdigit
884 --   $barcode      source barcode
885 --
886 -- If the source string is 13 or 14 characters long and contains only valid
887 -- Code 39 mod 43 characters, adds or replaces the 14th
888 -- character with a checkdigit computed according to the usual algorithm for library barcodes
889 -- using the Code 39 mod 43 symbology - see <http://en.wikipedia.org/wiki/Code_39#Code_39_mod_43>.  If the
890 -- input string does not meet those requirements, it is returned unchanged.
891 --
892 CREATE OR REPLACE FUNCTION migration_tools.add_code39mod43_checkdigit (TEXT) RETURNS TEXT AS $$
893     my $barcode = shift;
894
895     return $barcode if $barcode !~ /^[0-9A-Z. $\/+%-]{13,14}$/;
896     $barcode = substr($barcode, 0, 13); # ignore 14th character
897
898     my @valid_chars = split //, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%';
899     my %nums = map { $valid_chars[$_] => $_ } (0..42);
900
901     my $total = 0;
902     $total += $nums{$_} foreach split(//, $barcode);
903     my $remainder = $total % 43;
904     my $checkdigit = $valid_chars[$remainder];
905     return $barcode . $checkdigit;
906 $$ LANGUAGE PLPERLU STRICT STABLE;
907
908 CREATE OR REPLACE FUNCTION migration_tools.attempt_phone (TEXT,TEXT) RETURNS TEXT AS $$
909   DECLARE
910     phone TEXT := $1;
911     areacode TEXT := $2;
912     temp TEXT := '';
913     output TEXT := '';
914     n_digits INTEGER := 0;
915   BEGIN
916     temp := phone;
917     temp := REGEXP_REPLACE(temp, '^1*[^0-9]*(?=[0-9])', '');
918     temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2-\\3');
919     n_digits := LENGTH(REGEXP_REPLACE(REGEXP_REPLACE(temp, '(.*)?[a-zA-Z].*', E'\\1') , '[^0-9]', '', 'g'));
920     IF n_digits = 7 AND areacode <> '' THEN
921       temp := REGEXP_REPLACE(temp, '[^0-9]*([0-9]{3})[^0-9]*([0-9]{4})', E'\\1-\\2');
922       output := (areacode || '-' || temp);
923     ELSE
924       output := temp;
925     END IF;
926     RETURN output;
927   END;
928
929 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
930
931 CREATE OR REPLACE FUNCTION migration_tools.set_leader (TEXT, INT, TEXT) RETURNS TEXT AS $$
932   my ($marcxml, $pos, $value) = @_;
933
934   use MARC::Record;
935   use MARC::File::XML;
936
937   my $xml = $marcxml;
938   eval {
939     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
940     my $leader = $marc->leader();
941     substr($leader, $pos, 1) = $value;
942     $marc->leader($leader);
943     $xml = $marc->as_xml_record;
944     $xml =~ s/^<\?.+?\?>$//mo;
945     $xml =~ s/\n//sgo;
946     $xml =~ s/>\s+</></sgo;
947   };
948   return $xml;
949 $$ LANGUAGE PLPERLU STABLE;
950
951 CREATE OR REPLACE FUNCTION migration_tools.set_008 (TEXT, INT, TEXT) RETURNS TEXT AS $$
952   my ($marcxml, $pos, $value) = @_;
953
954   use MARC::Record;
955   use MARC::File::XML;
956
957   my $xml = $marcxml;
958   eval {
959     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
960     my $f008 = $marc->field('008');
961
962     if ($f008) {
963        my $field = $f008->data();
964        substr($field, $pos, 1) = $value;
965        $f008->update($field);
966        $xml = $marc->as_xml_record;
967        $xml =~ s/^<\?.+?\?>$//mo;
968        $xml =~ s/\n//sgo;
969        $xml =~ s/>\s+</></sgo;
970     }
971   };
972   return $xml;
973 $$ LANGUAGE PLPERLU STABLE;
974
975
976 CREATE OR REPLACE FUNCTION migration_tools.is_staff_profile (INT) RETURNS BOOLEAN AS $$
977   DECLARE
978     profile ALIAS FOR $1;
979   BEGIN
980     RETURN CASE WHEN 'Staff' IN (select (permission.grp_ancestors(profile)).name) THEN TRUE ELSE FALSE END;
981   END;
982 $$ LANGUAGE PLPGSQL STRICT STABLE;
983
984
985 CREATE OR REPLACE FUNCTION migration_tools.is_blank (TEXT) RETURNS BOOLEAN AS $$
986   BEGIN
987     RETURN CASE WHEN $1 = '' THEN TRUE ELSE FALSE END;
988   END;
989 $$ LANGUAGE PLPGSQL STRICT STABLE;
990
991
992 CREATE OR REPLACE FUNCTION migration_tools.insert_tags (TEXT, TEXT) RETURNS TEXT AS $$
993
994   my ($marcxml, $tags) = @_;
995
996   use MARC::Record;
997   use MARC::File::XML;
998
999   my $xml = $marcxml;
1000
1001   eval {
1002     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
1003     my $to_insert = MARC::Record->new_from_xml("<record>$tags</record>", 'UTF-8');
1004
1005     my @incumbents = ();
1006
1007     foreach my $field ( $marc->fields() ) {
1008       push @incumbents, $field->as_formatted();
1009     }
1010
1011     foreach $field ( $to_insert->fields() ) {
1012       if (!grep {$_ eq $field->as_formatted()} @incumbents) {
1013         $marc->insert_fields_ordered( ($field) );
1014       }
1015     }
1016
1017     $xml = $marc->as_xml_record;
1018     $xml =~ s/^<\?.+?\?>$//mo;
1019     $xml =~ s/\n//sgo;
1020     $xml =~ s/>\s+</></sgo;
1021   };
1022
1023   return $xml;
1024
1025 $$ LANGUAGE PLPERLU STABLE;
1026
1027 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix( tablename TEXT ) RETURNS VOID AS $$
1028
1029 -- Usage:
1030 --
1031 --   First make sure the circ matrix is loaded and the circulations
1032 --   have been staged to the extent possible (but at the very least
1033 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
1034 --   circ modifiers must also be in place.
1035 --
1036 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
1037 --
1038
1039 DECLARE
1040   circ_lib             INT;
1041   target_copy          INT;
1042   usr                  INT;
1043   is_renewal           BOOLEAN;
1044   this_duration_rule   INT;
1045   this_fine_rule       INT;
1046   this_max_fine_rule   INT;
1047   rcd                  config.rule_circ_duration%ROWTYPE;
1048   rrf                  config.rule_recurring_fine%ROWTYPE;
1049   rmf                  config.rule_max_fine%ROWTYPE;
1050   circ                 INT;
1051   n                    INT := 0;
1052   n_circs              INT;
1053   
1054 BEGIN
1055
1056   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
1057
1058   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
1059
1060     -- Fetch the correct rules for this circulation
1061     EXECUTE ('
1062       SELECT
1063         circ_lib,
1064         target_copy,
1065         usr,
1066         CASE
1067           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
1068           ELSE FALSE
1069         END
1070       FROM ' || tablename || ' WHERE id = ' || circ || ';')
1071       INTO circ_lib, target_copy, usr, is_renewal ;
1072     SELECT
1073       INTO this_duration_rule,
1074            this_fine_rule,
1075            this_max_fine_rule
1076       duration_rule,
1077       recurring_fine_rule,
1078       max_fine_rule
1079       FROM action.item_user_circ_test(
1080         circ_lib,
1081         target_copy,
1082         usr,
1083         is_renewal
1084         );
1085     SELECT INTO rcd * FROM config.rule_circ_duration
1086       WHERE id = this_duration_rule;
1087     SELECT INTO rrf * FROM config.rule_recurring_fine
1088       WHERE id = this_fine_rule;
1089     SELECT INTO rmf * FROM config.rule_max_fine
1090       WHERE id = this_max_fine_rule;
1091
1092     -- Apply the rules to this circulation
1093     EXECUTE ('UPDATE ' || tablename || ' c
1094     SET
1095       duration_rule = rcd.name,
1096       recurring_fine_rule = rrf.name,
1097       max_fine_rule = rmf.name,
1098       duration = rcd.normal,
1099       recurring_fine = rrf.normal,
1100       max_fine =
1101         CASE rmf.is_percent
1102           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
1103           ELSE rmf.amount
1104         END,
1105       renewal_remaining = rcd.max_renewals
1106     FROM
1107       config.rule_circ_duration rcd,
1108       config.rule_recurring_fine rrf,
1109       config.rule_max_fine rmf,
1110                         asset.copy ac
1111     WHERE
1112       rcd.id = ' || this_duration_rule || ' AND
1113       rrf.id = ' || this_fine_rule || ' AND
1114       rmf.id = ' || this_max_fine_rule || ' AND
1115                         ac.id = c.target_copy AND
1116       c.id = ' || circ || ';');
1117
1118     -- Keep track of where we are in the process
1119     n := n + 1;
1120     IF (n % 100 = 0) THEN
1121       RAISE INFO '%', n || ' of ' || n_circs
1122         || ' (' || (100*n/n_circs) || '%) circs updated.';
1123     END IF;
1124
1125   END LOOP;
1126
1127   RETURN;
1128 END;
1129
1130 $$ LANGUAGE plpgsql;
1131
1132 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix_before_20( tablename TEXT ) RETURNS VOID AS $$
1133
1134 -- Usage:
1135 --
1136 --   First make sure the circ matrix is loaded and the circulations
1137 --   have been staged to the extent possible (but at the very least
1138 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
1139 --   circ modifiers must also be in place.
1140 --
1141 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
1142 --
1143
1144 DECLARE
1145   circ_lib             INT;
1146   target_copy          INT;
1147   usr                  INT;
1148   is_renewal           BOOLEAN;
1149   this_duration_rule   INT;
1150   this_fine_rule       INT;
1151   this_max_fine_rule   INT;
1152   rcd                  config.rule_circ_duration%ROWTYPE;
1153   rrf                  config.rule_recurring_fine%ROWTYPE;
1154   rmf                  config.rule_max_fine%ROWTYPE;
1155   circ                 INT;
1156   n                    INT := 0;
1157   n_circs              INT;
1158   
1159 BEGIN
1160
1161   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
1162
1163   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
1164
1165     -- Fetch the correct rules for this circulation
1166     EXECUTE ('
1167       SELECT
1168         circ_lib,
1169         target_copy,
1170         usr,
1171         CASE
1172           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
1173           ELSE FALSE
1174         END
1175       FROM ' || tablename || ' WHERE id = ' || circ || ';')
1176       INTO circ_lib, target_copy, usr, is_renewal ;
1177     SELECT
1178       INTO this_duration_rule,
1179            this_fine_rule,
1180            this_max_fine_rule
1181       duration_rule,
1182       recuring_fine_rule,
1183       max_fine_rule
1184       FROM action.find_circ_matrix_matchpoint(
1185         circ_lib,
1186         target_copy,
1187         usr,
1188         is_renewal
1189         );
1190     SELECT INTO rcd * FROM config.rule_circ_duration
1191       WHERE id = this_duration_rule;
1192     SELECT INTO rrf * FROM config.rule_recurring_fine
1193       WHERE id = this_fine_rule;
1194     SELECT INTO rmf * FROM config.rule_max_fine
1195       WHERE id = this_max_fine_rule;
1196
1197     -- Apply the rules to this circulation
1198     EXECUTE ('UPDATE ' || tablename || ' c
1199     SET
1200       duration_rule = rcd.name,
1201       recuring_fine_rule = rrf.name,
1202       max_fine_rule = rmf.name,
1203       duration = rcd.normal,
1204       recuring_fine = rrf.normal,
1205       max_fine =
1206         CASE rmf.is_percent
1207           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
1208           ELSE rmf.amount
1209         END,
1210       renewal_remaining = rcd.max_renewals
1211     FROM
1212       config.rule_circ_duration rcd,
1213       config.rule_recuring_fine rrf,
1214       config.rule_max_fine rmf,
1215                         asset.copy ac
1216     WHERE
1217       rcd.id = ' || this_duration_rule || ' AND
1218       rrf.id = ' || this_fine_rule || ' AND
1219       rmf.id = ' || this_max_fine_rule || ' AND
1220                         ac.id = c.target_copy AND
1221       c.id = ' || circ || ';');
1222
1223     -- Keep track of where we are in the process
1224     n := n + 1;
1225     IF (n % 100 = 0) THEN
1226       RAISE INFO '%', n || ' of ' || n_circs
1227         || ' (' || (100*n/n_circs) || '%) circs updated.';
1228     END IF;
1229
1230   END LOOP;
1231
1232   RETURN;
1233 END;
1234
1235 $$ LANGUAGE plpgsql;
1236
1237 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix_after_20( tablename TEXT ) RETURNS VOID AS $$
1238
1239 -- Usage:
1240 --
1241 --   First make sure the circ matrix is loaded and the circulations
1242 --   have been staged to the extent possible (but at the very least
1243 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
1244 --   circ modifiers must also be in place.
1245 --
1246 --   SELECT migration_tools.apply_circ_matrix('m_pioneer.action_circulation');
1247 --
1248
1249 DECLARE
1250   circ_lib             INT;
1251   target_copy          INT;
1252   usr                  INT;
1253   is_renewal           BOOLEAN;
1254   this_duration_rule   INT;
1255   this_fine_rule       INT;
1256   this_max_fine_rule   INT;
1257   rcd                  config.rule_circ_duration%ROWTYPE;
1258   rrf                  config.rule_recurring_fine%ROWTYPE;
1259   rmf                  config.rule_max_fine%ROWTYPE;
1260   circ                 INT;
1261   n                    INT := 0;
1262   n_circs              INT;
1263   
1264 BEGIN
1265
1266   EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
1267
1268   FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
1269
1270     -- Fetch the correct rules for this circulation
1271     EXECUTE ('
1272       SELECT
1273         circ_lib,
1274         target_copy,
1275         usr,
1276         CASE
1277           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
1278           ELSE FALSE
1279         END
1280       FROM ' || tablename || ' WHERE id = ' || circ || ';')
1281       INTO circ_lib, target_copy, usr, is_renewal ;
1282     SELECT
1283       INTO this_duration_rule,
1284            this_fine_rule,
1285            this_max_fine_rule
1286       (matchpoint).duration_rule,
1287       (matchpoint).recurring_fine_rule,
1288       (matchpoint).max_fine_rule
1289       FROM action.find_circ_matrix_matchpoint(
1290         circ_lib,
1291         target_copy,
1292         usr,
1293         is_renewal
1294         );
1295     SELECT INTO rcd * FROM config.rule_circ_duration
1296       WHERE id = this_duration_rule;
1297     SELECT INTO rrf * FROM config.rule_recurring_fine
1298       WHERE id = this_fine_rule;
1299     SELECT INTO rmf * FROM config.rule_max_fine
1300       WHERE id = this_max_fine_rule;
1301
1302     -- Apply the rules to this circulation
1303     EXECUTE ('UPDATE ' || tablename || ' c
1304     SET
1305       duration_rule = rcd.name,
1306       recurring_fine_rule = rrf.name,
1307       max_fine_rule = rmf.name,
1308       duration = rcd.normal,
1309       recurring_fine = rrf.normal,
1310       max_fine =
1311         CASE rmf.is_percent
1312           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
1313           ELSE rmf.amount
1314         END,
1315       renewal_remaining = rcd.max_renewals,
1316       grace_period = rrf.grace_period
1317     FROM
1318       config.rule_circ_duration rcd,
1319       config.rule_recurring_fine rrf,
1320       config.rule_max_fine rmf,
1321                         asset.copy ac
1322     WHERE
1323       rcd.id = ' || this_duration_rule || ' AND
1324       rrf.id = ' || this_fine_rule || ' AND
1325       rmf.id = ' || this_max_fine_rule || ' AND
1326                         ac.id = c.target_copy AND
1327       c.id = ' || circ || ';');
1328
1329     -- Keep track of where we are in the process
1330     n := n + 1;
1331     IF (n % 100 = 0) THEN
1332       RAISE INFO '%', n || ' of ' || n_circs
1333         || ' (' || (100*n/n_circs) || '%) circs updated.';
1334     END IF;
1335
1336   END LOOP;
1337
1338   RETURN;
1339 END;
1340
1341 $$ LANGUAGE plpgsql;
1342
1343 CREATE OR REPLACE FUNCTION migration_tools.apply_circ_matrix_to_specific_circ( tablename TEXT, circ BIGINT ) RETURNS VOID AS $$
1344
1345 -- Usage:
1346 --
1347 --   First make sure the circ matrix is loaded and the circulations
1348 --   have been staged to the extent possible (but at the very least
1349 --   circ_lib, target_copy, usr, and *_renewal).  User profiles and
1350 --   circ modifiers must also be in place.
1351 --
1352 --   SELECT migration_tools.apply_circ_matrix_to_specific_circ('m_nwrl.action_circulation', 18391960);
1353 --
1354
1355 DECLARE
1356   circ_lib             INT;
1357   target_copy          INT;
1358   usr                  INT;
1359   is_renewal           BOOLEAN;
1360   this_duration_rule   INT;
1361   this_fine_rule       INT;
1362   this_max_fine_rule   INT;
1363   rcd                  config.rule_circ_duration%ROWTYPE;
1364   rrf                  config.rule_recurring_fine%ROWTYPE;
1365   rmf                  config.rule_max_fine%ROWTYPE;
1366   n                    INT := 0;
1367   n_circs              INT := 1;
1368   
1369 BEGIN
1370
1371   --EXECUTE 'SELECT COUNT(*) FROM ' || tablename || ';' INTO n_circs;
1372
1373   --FOR circ IN EXECUTE ('SELECT id FROM ' || tablename) LOOP
1374
1375     -- Fetch the correct rules for this circulation
1376     EXECUTE ('
1377       SELECT
1378         circ_lib,
1379         target_copy,
1380         usr,
1381         CASE
1382           WHEN phone_renewal OR desk_renewal OR opac_renewal THEN TRUE
1383           ELSE FALSE
1384         END
1385       FROM ' || tablename || ' WHERE id = ' || circ || ';')
1386       INTO circ_lib, target_copy, usr, is_renewal ;
1387     SELECT
1388       INTO this_duration_rule,
1389            this_fine_rule,
1390            this_max_fine_rule
1391       (matchpoint).duration_rule,
1392       (matchpoint).recurring_fine_rule,
1393       (matchpoint).max_fine_rule
1394       FROM action.find_circ_matrix_matchpoint(
1395         circ_lib,
1396         target_copy,
1397         usr,
1398         is_renewal
1399         );
1400     SELECT INTO rcd * FROM config.rule_circ_duration
1401       WHERE id = this_duration_rule;
1402     SELECT INTO rrf * FROM config.rule_recurring_fine
1403       WHERE id = this_fine_rule;
1404     SELECT INTO rmf * FROM config.rule_max_fine
1405       WHERE id = this_max_fine_rule;
1406
1407     -- Apply the rules to this circulation
1408     EXECUTE ('UPDATE ' || tablename || ' c
1409     SET
1410       duration_rule = rcd.name,
1411       recurring_fine_rule = rrf.name,
1412       max_fine_rule = rmf.name,
1413       duration = rcd.normal,
1414       recurring_fine = rrf.normal,
1415       max_fine =
1416         CASE rmf.is_percent
1417           WHEN TRUE THEN (rmf.amount / 100.0) * ac.price
1418           ELSE rmf.amount
1419         END,
1420       renewal_remaining = rcd.max_renewals,
1421       grace_period = rrf.grace_period
1422     FROM
1423       config.rule_circ_duration rcd,
1424       config.rule_recurring_fine rrf,
1425       config.rule_max_fine rmf,
1426                         asset.copy ac
1427     WHERE
1428       rcd.id = ' || this_duration_rule || ' AND
1429       rrf.id = ' || this_fine_rule || ' AND
1430       rmf.id = ' || this_max_fine_rule || ' AND
1431                         ac.id = c.target_copy AND
1432       c.id = ' || circ || ';');
1433
1434     -- Keep track of where we are in the process
1435     n := n + 1;
1436     IF (n % 100 = 0) THEN
1437       RAISE INFO '%', n || ' of ' || n_circs
1438         || ' (' || (100*n/n_circs) || '%) circs updated.';
1439     END IF;
1440
1441   --END LOOP;
1442
1443   RETURN;
1444 END;
1445
1446 $$ LANGUAGE plpgsql;
1447
1448
1449
1450
1451 CREATE OR REPLACE FUNCTION migration_tools.stage_not_applicable_asset_stat_cats( schemaname TEXT ) RETURNS VOID AS $$
1452
1453 -- USAGE: Make sure the stat_cat and stat_cat_entry tables are populated, including exactly one 'Not Applicable' entry per stat cat.
1454 --        Then SELECT migration_tools.stage_not_applicable_asset_stat_cats('m_foo');
1455
1456 -- TODO: Make a variant that will go directly to production tables -- which would be useful for retrofixing the absence of N/A cats.
1457 -- TODO: Add a similar tool for actor stat cats, which behave differently.
1458
1459 DECLARE
1460         c                    TEXT := schemaname || '.asset_copy_legacy';
1461         sc                                                                       TEXT := schemaname || '.asset_stat_cat';
1462         sce                                                                      TEXT := schemaname || '.asset_stat_cat_entry';
1463         scecm                                                            TEXT := schemaname || '.asset_stat_cat_entry_copy_map';
1464         stat_cat                                                 INT;
1465   stat_cat_entry       INT;
1466   
1467 BEGIN
1468
1469   FOR stat_cat IN EXECUTE ('SELECT id FROM ' || sc) LOOP
1470
1471                 EXECUTE ('SELECT id FROM ' || sce || ' WHERE stat_cat = ' || stat_cat || E' AND value = \'Not Applicable\';') INTO stat_cat_entry;
1472
1473                 EXECUTE ('INSERT INTO ' || scecm || ' (owning_copy, stat_cat, stat_cat_entry)
1474                                                         SELECT c.id, ' || stat_cat || ', ' || stat_cat_entry || ' FROM ' || c || ' c WHERE c.id NOT IN
1475                                                         (SELECT owning_copy FROM ' || scecm || ' WHERE stat_cat = ' || stat_cat || ');');
1476
1477   END LOOP;
1478
1479   RETURN;
1480 END;
1481
1482 $$ LANGUAGE plpgsql;
1483
1484 CREATE OR REPLACE FUNCTION migration_tools.assign_standing_penalties ( ) RETURNS VOID AS $$
1485
1486 -- USAGE: Once circulation data has been loaded, and group penalty thresholds have been set up, run this.
1487 --        This will assign standing penalties as needed.
1488
1489 DECLARE
1490   org_unit  INT;
1491   usr       INT;
1492
1493 BEGIN
1494
1495   FOR org_unit IN EXECUTE ('SELECT DISTINCT org_unit FROM permission.grp_penalty_threshold;') LOOP
1496
1497     FOR usr IN EXECUTE ('SELECT id FROM actor.usr WHERE NOT deleted;') LOOP
1498   
1499       EXECUTE('SELECT actor.calculate_system_penalties(' || usr || ', ' || org_unit || ');');
1500
1501     END LOOP;
1502
1503   END LOOP;
1504
1505   RETURN;
1506
1507 END;
1508
1509 $$ LANGUAGE plpgsql;
1510
1511
1512 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_pristine_database () RETURNS VOID AS $$
1513
1514 BEGIN
1515   INSERT INTO metabib.metarecord (fingerprint, master_record)
1516     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
1517       FROM  biblio.record_entry b
1518       WHERE NOT b.deleted
1519         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)
1520         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
1521       ORDER BY b.fingerprint, b.quality DESC;
1522   INSERT INTO metabib.metarecord_source_map (metarecord, source)
1523     SELECT  m.id, r.id
1524       FROM  biblio.record_entry r
1525       JOIN  metabib.metarecord m USING (fingerprint)
1526      WHERE  NOT r.deleted;
1527 END;
1528   
1529 $$ LANGUAGE plpgsql;
1530
1531
1532 CREATE OR REPLACE FUNCTION migration_tools.insert_metarecords_for_incumbent_database () RETURNS VOID AS $$
1533
1534 BEGIN
1535   INSERT INTO metabib.metarecord (fingerprint, master_record)
1536     SELECT  DISTINCT ON (b.fingerprint) b.fingerprint, b.id
1537       FROM  biblio.record_entry b
1538       WHERE NOT b.deleted
1539         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)
1540         AND NOT EXISTS ( SELECT 1 FROM metabib.metarecord WHERE fingerprint = b.fingerprint )
1541       ORDER BY b.fingerprint, b.quality DESC;
1542   INSERT INTO metabib.metarecord_source_map (metarecord, source)
1543     SELECT  m.id, r.id
1544       FROM  biblio.record_entry r
1545         JOIN metabib.metarecord m USING (fingerprint)
1546       WHERE NOT r.deleted
1547         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);
1548 END;
1549     
1550 $$ LANGUAGE plpgsql;
1551
1552
1553 CREATE OR REPLACE FUNCTION migration_tools.create_cards( schemaname TEXT ) RETURNS VOID AS $$
1554
1555 -- USAGE: Make sure the patrons are staged in schemaname.actor_usr_legacy and have 'usrname' assigned.
1556 --        Then SELECT migration_tools.create_cards('m_foo');
1557
1558 DECLARE
1559         u                    TEXT := schemaname || '.actor_usr_legacy';
1560         c                    TEXT := schemaname || '.actor_card';
1561   
1562 BEGIN
1563
1564         EXECUTE ('DELETE FROM ' || c || ';');
1565         EXECUTE ('INSERT INTO ' || c || ' (usr, barcode) SELECT id, usrname FROM ' || u || ';');
1566         EXECUTE ('UPDATE ' || u || ' u SET card = c.id FROM ' || c || ' c WHERE c.usr = u.id;');
1567
1568   RETURN;
1569
1570 END;
1571
1572 $$ LANGUAGE plpgsql;
1573
1574
1575 CREATE OR REPLACE FUNCTION migration_tools.insert_856_9_conditional (TEXT, TEXT) RETURNS TEXT AS $$
1576
1577   ## USAGE: UPDATE biblio.record_entry SET marc = migration_tools.insert_856_9(marc, 'ABC') WHERE [...];
1578
1579   my ($marcxml, $shortname) = @_;
1580
1581   use MARC::Record;
1582   use MARC::File::XML;
1583
1584   my $xml = $marcxml;
1585
1586   eval {
1587     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
1588
1589     foreach my $field ( $marc->field('856') ) {
1590       if ( scalar(grep( /(contentreserve|netlibrary|overdrive)\.com/i, $field->subfield('u'))) > 0 &&
1591            ! ( $field->as_string('9') =~ m/$shortname/ ) ) {
1592         $field->add_subfields( '9' => $shortname );
1593                                 $field->update( ind2 => '0');
1594       }
1595     }
1596
1597     $xml = $marc->as_xml_record;
1598     $xml =~ s/^<\?.+?\?>$//mo;
1599     $xml =~ s/\n//sgo;
1600     $xml =~ s/>\s+</></sgo;
1601   };
1602
1603   return $xml;
1604
1605 $$ LANGUAGE PLPERLU STABLE;
1606
1607 CREATE OR REPLACE FUNCTION migration_tools.insert_856_9 (TEXT, TEXT) RETURNS TEXT AS $$
1608
1609   ## USAGE: UPDATE biblio.record_entry SET marc = migration_tools.insert_856_9(marc, 'ABC') WHERE [...];
1610
1611   my ($marcxml, $shortname) = @_;
1612
1613   use MARC::Record;
1614   use MARC::File::XML;
1615
1616   my $xml = $marcxml;
1617
1618   eval {
1619     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
1620
1621     foreach my $field ( $marc->field('856') ) {
1622       if ( ! $field->as_string('9') ) {
1623         $field->add_subfields( '9' => $shortname );
1624       }
1625     }
1626
1627     $xml = $marc->as_xml_record;
1628     $xml =~ s/^<\?.+?\?>$//mo;
1629     $xml =~ s/\n//sgo;
1630     $xml =~ s/>\s+</></sgo;
1631   };
1632
1633   return $xml;
1634
1635 $$ LANGUAGE PLPERLU STABLE;
1636
1637
1638 CREATE OR REPLACE FUNCTION migration_tools.change_call_number(copy_id BIGINT, new_label TEXT, cn_class BIGINT) RETURNS VOID AS $$
1639
1640 DECLARE
1641   old_volume   BIGINT;
1642   new_volume   BIGINT;
1643   bib          BIGINT;
1644   owner        INTEGER;
1645   old_label    TEXT;
1646   remainder    BIGINT;
1647
1648 BEGIN
1649
1650   -- Bail out if asked to change the label to ##URI##
1651   IF new_label = '##URI##' THEN
1652     RETURN;
1653   END IF;
1654
1655   -- Gather information
1656   SELECT call_number INTO old_volume FROM asset.copy WHERE id = copy_id;
1657   SELECT record INTO bib FROM asset.call_number WHERE id = old_volume;
1658   SELECT owning_lib, label INTO owner, old_label FROM asset.call_number WHERE id = old_volume;
1659
1660   -- Bail out if the label already is ##URI##
1661   IF old_label = '##URI##' THEN
1662     RETURN;
1663   END IF;
1664
1665   -- Bail out if the call number label is already correct
1666   IF new_volume = old_volume THEN
1667     RETURN;
1668   END IF;
1669
1670   -- Check whether we already have a destination volume available
1671   SELECT id INTO new_volume FROM asset.call_number 
1672     WHERE 
1673       record = bib AND
1674       owning_lib = owner AND
1675       label = new_label AND
1676       NOT deleted;
1677
1678   -- Create destination volume if needed
1679   IF NOT FOUND THEN
1680     INSERT INTO asset.call_number (creator, editor, record, owning_lib, label, label_class) 
1681       VALUES (1, 1, bib, owner, new_label, cn_class);
1682     SELECT id INTO new_volume FROM asset.call_number
1683       WHERE 
1684         record = bib AND
1685         owning_lib = owner AND
1686         label = new_label AND
1687         NOT deleted;
1688   END IF;
1689
1690   -- Move copy to destination
1691   UPDATE asset.copy SET call_number = new_volume WHERE id = copy_id;
1692
1693   -- Delete source volume if it is now empty
1694   SELECT id INTO remainder FROM asset.copy WHERE call_number = old_volume AND NOT deleted;
1695   IF NOT FOUND THEN
1696     DELETE FROM asset.call_number WHERE id = old_volume;
1697   END IF;
1698
1699 END;
1700
1701 $$ LANGUAGE plpgsql;
1702
1703 CREATE OR REPLACE FUNCTION migration_tools.zip_to_city_state_county (TEXT) RETURNS TEXT[] AS $$
1704
1705         my $input = $_[0];
1706         my %zipdata;
1707
1708         open (FH, '<', '/openils/var/data/zips.txt') or return ('No File Found', 'No File Found', 'No File Found');
1709
1710         while (<FH>) {
1711                 chomp;
1712                 my ($junk, $state, $city, $zip, $foo, $bar, $county, $baz, $morejunk) = split(/\|/);
1713                 $zipdata{$zip} = [$city, $state, $county];
1714         }
1715
1716         if (defined $zipdata{$input}) {
1717                 my ($city, $state, $county) = @{$zipdata{$input}};
1718                 return [$city, $state, $county];
1719         } elsif (defined $zipdata{substr $input, 0, 5}) {
1720                 my ($city, $state, $county) = @{$zipdata{substr $input, 0, 5}};
1721                 return [$city, $state, $county];
1722         } else {
1723                 return ['ZIP not found', 'ZIP not found', 'ZIP not found'];
1724         }
1725   
1726 $$ LANGUAGE PLPERLU STABLE;
1727
1728 CREATE OR REPLACE FUNCTION migration_tools.check_ou_depth ( ) RETURNS VOID AS $$
1729
1730 DECLARE
1731   ou  INT;
1732         org_unit_depth INT;
1733         ou_parent INT;
1734         parent_depth INT;
1735   errors_found BOOLEAN;
1736         ou_shortname TEXT;
1737         parent_shortname TEXT;
1738         ou_type_name TEXT;
1739         parent_type TEXT;
1740         type_id INT;
1741         type_depth INT;
1742         type_parent INT;
1743         type_parent_depth INT;
1744         proper_parent TEXT;
1745
1746 BEGIN
1747
1748         errors_found := FALSE;
1749
1750 -- Checking actor.org_unit_type
1751
1752         FOR type_id IN EXECUTE ('SELECT id FROM actor.org_unit_type ORDER BY id;') LOOP
1753
1754                 SELECT depth FROM actor.org_unit_type WHERE id = type_id INTO type_depth;
1755                 SELECT parent FROM actor.org_unit_type WHERE id = type_id INTO type_parent;
1756
1757                 IF type_parent IS NOT NULL THEN
1758
1759                         SELECT depth FROM actor.org_unit_type WHERE id = type_parent INTO type_parent_depth;
1760
1761                         IF type_depth - type_parent_depth <> 1 THEN
1762                                 SELECT name FROM actor.org_unit_type WHERE id = type_id INTO ou_type_name;
1763                                 SELECT name FROM actor.org_unit_type WHERE id = type_parent INTO parent_type;
1764                                 RAISE INFO 'The % org unit type has a depth of %, but its parent org unit type, %, has a depth of %.',
1765                                         ou_type_name, type_depth, parent_type, type_parent_depth;
1766                                 errors_found := TRUE;
1767
1768                         END IF;
1769
1770                 END IF;
1771
1772         END LOOP;
1773
1774 -- Checking actor.org_unit
1775
1776   FOR ou IN EXECUTE ('SELECT id FROM actor.org_unit ORDER BY shortname;') LOOP
1777
1778                 SELECT parent_ou FROM actor.org_unit WHERE id = ou INTO ou_parent;
1779                 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;
1780                 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;
1781                 SELECT shortname FROM actor.org_unit WHERE id = ou INTO ou_shortname;
1782                 SELECT shortname FROM actor.org_unit WHERE id = ou_parent INTO parent_shortname;
1783                 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;
1784                 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;
1785
1786                 IF ou_parent IS NOT NULL THEN
1787
1788                         IF      (org_unit_depth - parent_depth <> 1) OR (
1789                                 (SELECT parent FROM actor.org_unit_type WHERE name = ou_type_name) <> (SELECT id FROM actor.org_unit_type WHERE name = parent_type)
1790                         ) THEN
1791                                 RAISE INFO '% (org unit %) is a % (depth %) but its parent, % (org unit %), is a % (depth %).', 
1792                                         ou_shortname, ou, ou_type_name, org_unit_depth, parent_shortname, ou_parent, parent_type, parent_depth;
1793                                 errors_found := TRUE;
1794                         END IF;
1795
1796                 END IF;
1797
1798   END LOOP;
1799
1800         IF NOT errors_found THEN
1801                 RAISE INFO 'No errors found.';
1802         END IF;
1803
1804   RETURN;
1805
1806 END;
1807
1808 $$ LANGUAGE plpgsql;
1809
1810
1811 CREATE OR REPLACE FUNCTION migration_tools.refresh_opac_visible_copies ( ) RETURNS VOID AS $$
1812
1813 BEGIN   
1814
1815         DELETE FROM asset.opac_visible_copies;
1816
1817         INSERT INTO asset.opac_visible_copies (id, circ_lib, record)
1818                 SELECT DISTINCT
1819                         cp.id, cp.circ_lib, cn.record
1820                 FROM
1821                         asset.copy cp
1822                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
1823                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
1824                         JOIN asset.copy_location cl ON (cp.location = cl.id)
1825                         JOIN config.copy_status cs ON (cp.status = cs.id)
1826                         JOIN biblio.record_entry b ON (cn.record = b.id)
1827                 WHERE 
1828                         NOT cp.deleted AND
1829                         NOT cn.deleted AND
1830                         NOT b.deleted AND
1831                         cs.opac_visible AND
1832                         cl.opac_visible AND
1833                         cp.opac_visible AND
1834                         a.opac_visible AND
1835                         cp.id NOT IN (SELECT id FROM asset.opac_visible_copies);
1836
1837 END;
1838
1839 $$ LANGUAGE plpgsql;
1840
1841
1842 CREATE OR REPLACE FUNCTION migration_tools.change_owning_lib(copy_id BIGINT, new_owning_lib INTEGER) RETURNS VOID AS $$
1843
1844 DECLARE
1845   old_volume     BIGINT;
1846   new_volume     BIGINT;
1847   bib            BIGINT;
1848   old_owning_lib INTEGER;
1849         old_label      TEXT;
1850   remainder      BIGINT;
1851
1852 BEGIN
1853
1854   -- Gather information
1855   SELECT call_number INTO old_volume FROM asset.copy WHERE id = copy_id;
1856   SELECT record INTO bib FROM asset.call_number WHERE id = old_volume;
1857   SELECT owning_lib, label INTO old_owning_lib, old_label FROM asset.call_number WHERE id = old_volume;
1858
1859         -- Bail out if the new_owning_lib is not the ID of an org_unit
1860         IF new_owning_lib NOT IN (SELECT id FROM actor.org_unit) THEN
1861                 RAISE WARNING 
1862                         '% is not a valid actor.org_unit ID; no change made.', 
1863                                 new_owning_lib;
1864                 RETURN;
1865         END IF;
1866
1867   -- Bail out discreetly if the owning_lib is already correct
1868   IF new_owning_lib = old_owning_lib THEN
1869     RETURN;
1870   END IF;
1871
1872   -- Check whether we already have a destination volume available
1873   SELECT id INTO new_volume FROM asset.call_number 
1874     WHERE 
1875       record = bib AND
1876       owning_lib = new_owning_lib AND
1877       label = old_label AND
1878       NOT deleted;
1879
1880   -- Create destination volume if needed
1881   IF NOT FOUND THEN
1882     INSERT INTO asset.call_number (creator, editor, record, owning_lib, label) 
1883       VALUES (1, 1, bib, new_owning_lib, old_label);
1884     SELECT id INTO new_volume FROM asset.call_number
1885       WHERE 
1886         record = bib AND
1887         owning_lib = new_owning_lib AND
1888         label = old_label AND
1889         NOT deleted;
1890   END IF;
1891
1892   -- Move copy to destination
1893   UPDATE asset.copy SET call_number = new_volume WHERE id = copy_id;
1894
1895   -- Delete source volume if it is now empty
1896   SELECT id INTO remainder FROM asset.copy WHERE call_number = old_volume AND NOT deleted;
1897   IF NOT FOUND THEN
1898     DELETE FROM asset.call_number WHERE id = old_volume;
1899   END IF;
1900
1901 END;
1902
1903 $$ LANGUAGE plpgsql;
1904
1905
1906 CREATE OR REPLACE FUNCTION migration_tools.change_owning_lib(copy_id BIGINT, new_owner TEXT) RETURNS VOID AS $$
1907
1908 -- You can use shortnames with this function, which looks up the org unit ID and passes it to change_owning_lib(BIGINT,INTEGER).
1909
1910 DECLARE
1911         new_owning_lib  INTEGER;
1912
1913 BEGIN
1914
1915         -- Parse the new_owner as an org unit ID or shortname
1916         IF new_owner IN (SELECT shortname FROM actor.org_unit) THEN
1917                 SELECT id INTO new_owning_lib FROM actor.org_unit WHERE shortname = new_owner;
1918                 PERFORM migration_tools.change_owning_lib(copy_id, new_owning_lib);
1919         ELSIF new_owner ~ E'^[0-9]+$' THEN
1920                 IF new_owner::INTEGER IN (SELECT id FROM actor.org_unit) THEN
1921                         RAISE INFO 
1922                                 '%',
1923                                 E'You don\'t need to put the actor.org_unit ID in quotes; '
1924                                         || E'if you put it in quotes, I\'m going to try to parse it as a shortname first.';
1925                         new_owning_lib := new_owner::INTEGER;
1926                 PERFORM migration_tools.change_owning_lib(copy_id, new_owning_lib);
1927                 END IF;
1928         ELSE
1929                 RAISE WARNING 
1930                         '% is not a valid actor.org_unit shortname or ID; no change made.', 
1931                         new_owning_lib;
1932                 RETURN;
1933         END IF;
1934
1935 END;
1936
1937 $$ LANGUAGE plpgsql;
1938
1939 CREATE OR REPLACE FUNCTION migration_tools.marc_parses( TEXT ) RETURNS BOOLEAN AS $func$
1940
1941 use MARC::Record;
1942 use MARC::File::XML (BinaryEncoding => 'UTF-8');
1943 use MARC::Charset;
1944
1945 MARC::Charset->assume_unicode(1);
1946
1947 my $xml = shift;
1948
1949 eval {
1950     my $r = MARC::Record->new_from_xml( $xml );
1951     my $output_xml = $r->as_xml_record();
1952 };
1953 if ($@) {
1954     return 0;
1955 } else {
1956     return 1;
1957 }
1958
1959 $func$ LANGUAGE PLPERLU;
1960 COMMENT ON FUNCTION migration_tools.marc_parses(TEXT) IS 'Return boolean indicating if MARCXML string is parseable by MARC::File::XML';
1961
1962 CREATE OR REPLACE FUNCTION migration_tools.simple_export_library_config(dir TEXT, orgs INT[]) RETURNS VOID AS $FUNC$
1963 BEGIN
1964    EXECUTE $$COPY (SELECT * FROM actor.hours_of_operation WHERE id IN ($$ ||
1965            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1966            $$) TO '$$ ||  dir || $$/actor_hours_of_operation'$$;
1967    EXECUTE $$COPY (SELECT org_unit, close_start, close_end, reason FROM actor.org_unit_closed WHERE org_unit IN ($$ ||
1968            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1969            $$) TO '$$ ||  dir || $$/actor_org_unit_closed'$$;
1970    EXECUTE $$COPY (SELECT org_unit, name, value FROM actor.org_unit_setting WHERE org_unit IN ($$ ||
1971            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1972            $$) TO '$$ ||  dir || $$/actor_org_unit_setting'$$;
1973    EXECUTE $$COPY (SELECT name, owning_lib, holdable, hold_verify, opac_visible, circulate FROM asset.copy_location WHERE owning_lib IN ($$ ||
1974            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1975            $$) TO '$$ ||  dir || $$/asset_copy_location'$$;
1976    EXECUTE $$COPY (SELECT grp, org_unit, penalty, threshold FROM permission.grp_penalty_threshold WHERE org_unit IN ($$ ||
1977            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1978            $$) TO '$$ ||  dir || $$/permission_grp_penalty_threshold'$$;
1979    EXECUTE $$COPY (SELECT owning_lib, label, label_sortkey FROM asset.call_number_prefix WHERE owning_lib IN ($$ ||
1980            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1981            $$) TO '$$ ||  dir || $$/asset_call_number_prefix'$$;
1982    EXECUTE $$COPY (SELECT owning_lib, label, label_sortkey FROM asset.call_number_suffix WHERE owning_lib IN ($$ ||
1983            ARRAY_TO_STRING(orgs, ',') || $$)$$ ||
1984            $$) TO '$$ ||  dir || $$/asset_call_number_suffix'$$;
1985    EXECUTE $$COPY config.rule_circ_duration TO '$$ ||  dir || $$/config_rule_circ_duration'$$;
1986    EXECUTE $$COPY config.rule_age_hold_protect TO '$$ ||  dir || $$/config_rule_age_hold_protect'$$;
1987    EXECUTE $$COPY config.rule_max_fine TO '$$ ||  dir || $$/config_rule_max_fine'$$;
1988    EXECUTE $$COPY config.rule_recurring_fine TO '$$ ||  dir || $$/config_rule_recurring_fine'$$;
1989    EXECUTE $$COPY permission.grp_tree TO '$$ ||  dir || $$/permission_grp_tree'$$;
1990 END;
1991 $FUNC$ LANGUAGE PLPGSQL;
1992
1993 CREATE OR REPLACE FUNCTION migration_tools.simple_import_library_config(dir TEXT) RETURNS VOID AS $FUNC$
1994 BEGIN
1995    EXECUTE $$COPY actor.hours_of_operation FROM '$$ ||  dir || $$/actor_hours_of_operation'$$;
1996    EXECUTE $$COPY actor.org_unit_closed (org_unit, close_start, close_end, reason) FROM '$$ ||  dir || $$/actor_org_unit_closed'$$;
1997    EXECUTE $$COPY actor.org_unit_setting (org_unit, name, value) FROM '$$ ||  dir || $$/actor_org_unit_setting'$$;
1998    EXECUTE $$COPY asset.copy_location (name, owning_lib, holdable, hold_verify, opac_visible, circulate) FROM '$$ ||  dir || $$/asset_copy_location'$$;
1999    EXECUTE $$COPY permission.grp_penalty_threshold (grp, org_unit, penalty, threshold) FROM '$$ ||  dir || $$/permission_grp_penalty_threshold'$$;
2000    EXECUTE $$COPY asset.call_number_prefix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/asset_call_number_prefix'$$;
2001    EXECUTE $$COPY asset.call_number_suffix (owning_lib, label, label_sortkey) FROM '$$ ||  dir || $$/asset_call_number_suffix'$$;
2002
2003    -- import any new circ rules
2004    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_circ_duration', 'id', 'name');
2005    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_age_hold_protect', 'id', 'name');
2006    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_max_fine', 'id', 'name');
2007    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'config', 'rule_recurring_fine', 'id', 'name');
2008
2009    -- and permission groups
2010    PERFORM migration_tools.simple_import_new_rows_by_value(dir, 'permission', 'grp_tree', 'id', 'name');
2011
2012 END;
2013 $FUNC$ LANGUAGE PLPGSQL;
2014
2015
2016 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$
2017 DECLARE
2018     name TEXT;
2019     loopq TEXT;
2020     existsq TEXT;
2021     ct INTEGER;
2022     cols TEXT[];
2023     copyst TEXT;
2024 BEGIN
2025     EXECUTE $$DROP TABLE IF EXISTS tmp_$$ || tablename;
2026     EXECUTE $$CREATE TEMPORARY TABLE tmp_$$ || tablename || $$ AS SELECT * FROM $$ || schemaname || '.' || tablename || $$ LIMIT 0$$;
2027     EXECUTE $$COPY tmp_$$ || tablename || $$ FROM '$$ ||  dir || '/' || schemaname || '_' || tablename || $$'$$;
2028     loopq := 'SELECT ' || matchcol || ' FROM tmp_' || tablename || ' ORDER BY ' || idcol;
2029     existsq := 'SELECT COUNT(*) FROM ' || schemaname || '.' || tablename || ' WHERE ' || matchcol || ' = $1';
2030     SELECT ARRAY_AGG(column_name::TEXT) INTO cols FROM information_schema.columns WHERE table_schema = schemaname AND table_name = tablename AND column_name <> idcol;
2031     FOR name IN EXECUTE loopq LOOP
2032        EXECUTE existsq INTO ct USING name;
2033        IF ct = 0 THEN
2034            RAISE NOTICE 'inserting %.% row for %', schemaname, tablename, name;
2035            copyst := 'INSERT INTO ' || schemaname || '.' || tablename || ' (' || ARRAY_TO_STRING(cols, ',') || ') SELECT ' || ARRAY_TO_STRING(cols, ',') || 
2036                      ' FROM tmp_' || tablename || ' WHERE ' || matchcol || ' = $1';
2037            EXECUTE copyst USING name;
2038        END IF;
2039     END LOOP;
2040 END;
2041 $FUNC$ LANGUAGE PLPGSQL;
2042
2043 CREATE OR REPLACE FUNCTION migration_tools.merge_marc_fields( TEXT, TEXT, TEXT[] ) RETURNS TEXT AS $func$
2044
2045 use strict;
2046 use warnings;
2047
2048 use MARC::Record;
2049 use MARC::File::XML (BinaryEncoding => 'UTF-8');
2050 use MARC::Charset;
2051
2052 MARC::Charset->assume_unicode(1);
2053
2054 my $target_xml = shift;
2055 my $source_xml = shift;
2056 my $tags = shift;
2057
2058 my $target;
2059 my $source;
2060
2061 eval { $target = MARC::Record->new_from_xml( $target_xml ); };
2062 if ($@) {
2063     return;
2064 }
2065 eval { $source = MARC::Record->new_from_xml( $source_xml ); };
2066 if ($@) {
2067     return;
2068 }
2069
2070 my $source_id = $source->subfield('901', 'c');
2071 $source_id = $source->subfield('903', 'a') unless $source_id;
2072 my $target_id = $target->subfield('901', 'c');
2073 $target_id = $target->subfield('903', 'a') unless $target_id;
2074
2075 my %existing_fields;
2076 foreach my $tag (@$tags) {
2077     my %existing_fields = map { $_->as_formatted() => 1 } $target->field($tag);
2078     my @to_add = grep { not exists $existing_fields{$_->as_formatted()} } $source->field($tag);
2079     $target->insert_fields_ordered(map { $_->clone() } @to_add);
2080     if (@to_add) {
2081         elog(NOTICE, "Merged $tag tag(s) from $source_id to $target_id");
2082     }
2083 }
2084
2085 my $xml = $target->as_xml_record;
2086 $xml =~ s/^<\?.+?\?>$//mo;
2087 $xml =~ s/\n//sgo;
2088 $xml =~ s/>\s+</></sgo;
2089
2090 return $xml;
2091
2092 $func$ LANGUAGE PLPERLU;
2093 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.';
2094
2095 CREATE OR REPLACE FUNCTION migration_tools.make_stub_bib (text[], text[]) RETURNS TEXT AS $func$
2096
2097 use strict;
2098 use warnings;
2099
2100 use MARC::Record;
2101 use MARC::File::XML (BinaryEncoding => 'UTF-8');
2102 use Text::CSV;
2103
2104 my $in_tags = shift;
2105 my $in_values = shift;
2106
2107 # hack-and-slash parsing of array-passed-as-string;
2108 # this can go away once everybody is running Postgres 9.1+
2109 my $csv = Text::CSV->new({binary => 1});
2110 $in_tags =~ s/^{//;
2111 $in_tags =~ s/}$//;
2112 my $status = $csv->parse($in_tags);
2113 my $tags = [ $csv->fields() ];
2114 $in_values =~ s/^{//;
2115 $in_values =~ s/}$//;
2116 $status = $csv->parse($in_values);
2117 my $values = [ $csv->fields() ];
2118
2119 my $marc = MARC::Record->new();
2120
2121 $marc->leader('00000nam a22000007  4500');
2122 $marc->append_fields(MARC::Field->new('008', '000000s                       000   eng d'));
2123
2124 foreach my $i (0..$#$tags) {
2125     my ($tag, $sf);
2126     if ($tags->[$i] =~ /^(\d{3})([0-9a-z])$/) {
2127         $tag = $1;
2128         $sf = $2;
2129         $marc->append_fields(MARC::Field->new($tag, ' ', ' ', $sf => $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
2130     } elsif ($tags->[$i] =~ /^(\d{3})$/) {
2131         $tag = $1;
2132         $marc->append_fields(MARC::Field->new($tag, $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
2133     }
2134 }
2135
2136 my $xml = $marc->as_xml_record;
2137 $xml =~ s/^<\?.+?\?>$//mo;
2138 $xml =~ s/\n//sgo;
2139 $xml =~ s/>\s+</></sgo;
2140
2141 return $xml;
2142
2143 $func$ LANGUAGE PLPERLU;
2144 COMMENT ON FUNCTION migration_tools.make_stub_bib (text[], text[]) IS $$Simple function to create a stub MARCXML bib from a set of columns.
2145 The first argument is an array of tag/subfield specifiers, e.g., ARRAY['001', '245a', '500a'].
2146 The second argument is an array of text containing the values to plug into each field.  
2147 If the value for a given field is NULL or the empty string, it is not inserted.
2148 $$;
2149
2150 CREATE OR REPLACE FUNCTION migration_tools.set_indicator (TEXT, TEXT, INTEGER, CHAR(1)) RETURNS TEXT AS $func$
2151
2152 my ($marcxml, $tag, $pos, $value) = @_;
2153
2154 use MARC::Record;
2155 use MARC::File::XML (BinaryEncoding => 'UTF-8');
2156 use MARC::Charset;
2157 use strict;
2158
2159 MARC::Charset->assume_unicode(1);
2160
2161 elog(ERROR, 'indicator position must be either 1 or 2') unless $pos =~ /^[12]$/;
2162 elog(ERROR, 'MARC tag must be numeric') unless $tag =~ /^\d{3}$/;
2163 elog(ERROR, 'MARC tag must not be control field') if $tag =~ /^00/;
2164 elog(ERROR, 'Value must be exactly one character') unless $value =~ /^.$/;
2165
2166 my $xml = $marcxml;
2167 eval {
2168     my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
2169
2170     foreach my $field ($marc->field($tag)) {
2171         $field->update("ind$pos" => $value);
2172     }
2173     $xml = $marc->as_xml_record;
2174     $xml =~ s/^<\?.+?\?>$//mo;
2175     $xml =~ s/\n//sgo;
2176     $xml =~ s/>\s+</></sgo;
2177 };
2178 return $xml;
2179
2180 $func$ LANGUAGE PLPERLU;
2181
2182 COMMENT ON FUNCTION migration_tools.set_indicator(TEXT, TEXT, INTEGER, CHAR(1)) IS $$Set indicator value of a specified MARC field.
2183 The first argument is a MARCXML string.
2184 The second argument is a MARC tag.
2185 The third argument is the indicator position, either 1 or 2.
2186 The fourth argument is the character to set the indicator value to.
2187 All occurences of the specified field will be changed.
2188 The function returns the revised MARCXML string.$$;
2189
2190 CREATE OR REPLACE FUNCTION migration_tools.create_staff_user(
2191     username TEXT,
2192     password TEXT,
2193     org TEXT,
2194     perm_group TEXT,
2195     first_name TEXT DEFAULT '',
2196     last_name TEXT DEFAULT ''
2197 ) RETURNS VOID AS $func$
2198 BEGIN
2199     RAISE NOTICE '%', org ;
2200     INSERT INTO actor.usr (usrname, passwd, ident_type, first_given_name, family_name, home_ou, profile)
2201     SELECT username, password, 1, first_name, last_name, aou.id, pgt.id
2202     FROM   actor.org_unit aou, permission.grp_tree pgt
2203     WHERE  aou.shortname = org
2204     AND    pgt.name = perm_group;
2205 END
2206 $func$
2207 LANGUAGE PLPGSQL;
2208
2209 -- example: SELECT * FROM migration_tools.duplicate_template(5,'{3,4}');
2210 CREATE OR REPLACE FUNCTION migration_tools.duplicate_template (INTEGER, INTEGER[]) RETURNS VOID AS $$
2211     DECLARE
2212         target_event_def ALIAS FOR $1;
2213         orgs ALIAS FOR $2;
2214     BEGIN
2215         DROP TABLE IF EXISTS new_atevdefs;
2216         CREATE TEMP TABLE new_atevdefs (atevdef INTEGER);
2217         FOR i IN array_lower(orgs,1) .. array_upper(orgs,1) LOOP
2218             INSERT INTO action_trigger.event_definition (
2219                 active
2220                 ,owner
2221                 ,name
2222                 ,hook
2223                 ,validator
2224                 ,reactor
2225                 ,cleanup_success
2226                 ,cleanup_failure
2227                 ,delay
2228                 ,max_delay
2229                 ,usr_field
2230                 ,opt_in_setting
2231                 ,delay_field
2232                 ,group_field
2233                 ,template
2234                 ,granularity
2235                 ,repeat_delay
2236             ) SELECT
2237                 'f'
2238                 ,orgs[i]
2239                 ,name || ' (clone of '||target_event_def||')'
2240                 ,hook
2241                 ,validator
2242                 ,reactor
2243                 ,cleanup_success
2244                 ,cleanup_failure
2245                 ,delay
2246                 ,max_delay
2247                 ,usr_field
2248                 ,opt_in_setting
2249                 ,delay_field
2250                 ,group_field
2251                 ,template
2252                 ,granularity
2253                 ,repeat_delay
2254             FROM
2255                 action_trigger.event_definition
2256             WHERE
2257                 id = target_event_def
2258             ;
2259             RAISE INFO 'created atevdef with id = %', currval('action_trigger.event_definition_id_seq');
2260             INSERT INTO new_atevdefs SELECT currval('action_trigger.event_definition_id_seq');
2261             INSERT INTO action_trigger.environment (
2262                 event_def
2263                 ,path
2264                 ,collector
2265                 ,label
2266             ) SELECT
2267                 currval('action_trigger.event_definition_id_seq')
2268                 ,path
2269                 ,collector
2270                 ,label
2271             FROM
2272                 action_trigger.environment
2273             WHERE
2274                 event_def = target_event_def
2275             ;
2276             INSERT INTO action_trigger.event_params (
2277                 event_def
2278                 ,param
2279                 ,value
2280             ) SELECT
2281                 currval('action_trigger.event_definition_id_seq')
2282                 ,param
2283                 ,value
2284             FROM
2285                 action_trigger.event_params
2286             WHERE
2287                 event_def = target_event_def
2288             ;
2289         END LOOP;
2290         RAISE INFO '-- UPDATE action_trigger.event_definition SET active = CASE WHEN id = % THEN FALSE ELSE TRUE END WHERE id in (%,%);', target_event_def, target_event_def, (SELECT array_to_string(array_agg(atevdef),',') from new_atevdefs);
2291     END;
2292 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
2293
2294 -- example: SELECT * FROM migration_tools.duplicate_template_but_change_delay(5,'{3,4}','00:30:00'::INTERVAL);
2295 CREATE OR REPLACE FUNCTION migration_tools.duplicate_template_but_change_delay (INTEGER, INTEGER[], INTERVAL) RETURNS VOID AS $$
2296     DECLARE
2297         target_event_def ALIAS FOR $1;
2298         orgs ALIAS FOR $2;
2299         new_interval ALIAS FOR $3;
2300     BEGIN
2301         DROP TABLE IF EXISTS new_atevdefs;
2302         CREATE TEMP TABLE new_atevdefs (atevdef INTEGER);
2303         FOR i IN array_lower(orgs,1) .. array_upper(orgs,1) LOOP
2304             INSERT INTO action_trigger.event_definition (
2305                 active
2306                 ,owner
2307                 ,name
2308                 ,hook
2309                 ,validator
2310                 ,reactor
2311                 ,cleanup_success
2312                 ,cleanup_failure
2313                 ,delay
2314                 ,max_delay
2315                 ,usr_field
2316                 ,opt_in_setting
2317                 ,delay_field
2318                 ,group_field
2319                 ,template
2320                 ,granularity
2321                 ,repeat_delay
2322             ) SELECT
2323                 'f'
2324                 ,orgs[i]
2325                 ,name || ' (clone of '||target_event_def||')'
2326                 ,hook
2327                 ,validator
2328                 ,reactor
2329                 ,cleanup_success
2330                 ,cleanup_failure
2331                 ,new_interval
2332                 ,max_delay
2333                 ,usr_field
2334                 ,opt_in_setting
2335                 ,delay_field
2336                 ,group_field
2337                 ,template
2338                 ,granularity
2339                 ,repeat_delay
2340             FROM
2341                 action_trigger.event_definition
2342             WHERE
2343                 id = target_event_def
2344             ;
2345             RAISE INFO 'created atevdef with id = %', currval('action_trigger.event_definition_id_seq');
2346             INSERT INTO new_atevdefs SELECT currval('action_trigger.event_definition_id_seq');
2347             INSERT INTO action_trigger.environment (
2348                 event_def
2349                 ,path
2350                 ,collector
2351                 ,label
2352             ) SELECT
2353                 currval('action_trigger.event_definition_id_seq')
2354                 ,path
2355                 ,collector
2356                 ,label
2357             FROM
2358                 action_trigger.environment
2359             WHERE
2360                 event_def = target_event_def
2361             ;
2362             INSERT INTO action_trigger.event_params (
2363                 event_def
2364                 ,param
2365                 ,value
2366             ) SELECT
2367                 currval('action_trigger.event_definition_id_seq')
2368                 ,param
2369                 ,value
2370             FROM
2371                 action_trigger.event_params
2372             WHERE
2373                 event_def = target_event_def
2374             ;
2375         END LOOP;
2376         RAISE INFO '-- UPDATE action_trigger.event_definition SET active = CASE WHEN id = % THEN FALSE ELSE TRUE END WHERE id in (%,%);', target_event_def, target_event_def, (SELECT array_to_string(array_agg(atevdef),',') from new_atevdefs);
2377     END;
2378 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
2379
2380 CREATE OR REPLACE FUNCTION migration_tools.get_marc_tag (TEXT, TEXT, TEXT, TEXT) RETURNS TEXT AS $$
2381     my ($marcxml, $tag, $subfield, $delimiter) = @_;
2382
2383     use MARC::Record;
2384     use MARC::File::XML;
2385     use MARC::Field;
2386
2387     my $field;
2388     eval {
2389         my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
2390         $field = $marc->field($tag);
2391     };
2392     return $field->as_string($subfield,$delimiter);
2393 $$ LANGUAGE PLPERLU STABLE;
2394
2395 CREATE OR REPLACE FUNCTION migration_tools.get_marc_tags (TEXT, TEXT, TEXT, TEXT) RETURNS TEXT[] AS $$
2396     my ($marcxml, $tag, $subfield, $delimiter) = @_;
2397
2398     use MARC::Record;
2399     use MARC::File::XML;
2400     use MARC::Field;
2401
2402     my @fields;
2403     eval {
2404         my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
2405         @fields = $marc->field($tag);
2406     };
2407     my @texts;
2408     foreach my $field (@fields) {
2409         push @texts, $field->as_string($subfield,$delimiter);
2410     }
2411     return \@texts;
2412 $$ LANGUAGE PLPERLU STABLE;
2413
2414 CREATE OR REPLACE FUNCTION migration_tools.find_hold_matrix_matchpoint (INTEGER) RETURNS INTEGER AS $$
2415     SELECT action.find_hold_matrix_matchpoint(
2416         (SELECT pickup_lib FROM action.hold_request WHERE id = $1),
2417         (SELECT request_lib FROM action.hold_request WHERE id = $1),
2418         (SELECT current_copy FROM action.hold_request WHERE id = $1),
2419         (SELECT usr FROM action.hold_request WHERE id = $1),
2420         (SELECT requestor FROM action.hold_request WHERE id = $1)
2421     );
2422 $$ LANGUAGE SQL;
2423
2424 CREATE OR REPLACE FUNCTION migration_tools.find_hold_matrix_matchpoint2 (INTEGER) RETURNS SETOF action.matrix_test_result AS $$
2425     SELECT action.hold_request_permit_test(
2426         (SELECT pickup_lib FROM action.hold_request WHERE id = $1),
2427         (SELECT request_lib FROM action.hold_request WHERE id = $1),
2428         (SELECT current_copy FROM action.hold_request WHERE id = $1),
2429         (SELECT usr FROM action.hold_request WHERE id = $1),
2430         (SELECT requestor FROM action.hold_request WHERE id = $1)
2431     );
2432 $$ LANGUAGE SQL;
2433
2434 CREATE OR REPLACE FUNCTION migration_tools.find_circ_matrix_matchpoint (INTEGER) RETURNS SETOF action.found_circ_matrix_matchpoint AS $$
2435     SELECT action.find_circ_matrix_matchpoint(
2436         (SELECT circ_lib FROM action.circulation WHERE id = $1),
2437         (SELECT target_copy FROM action.circulation WHERE id = $1),
2438         (SELECT usr FROM action.circulation WHERE id = $1),
2439         (SELECT COALESCE(
2440                 NULLIF(phone_renewal,false),
2441                 NULLIF(desk_renewal,false),
2442                 NULLIF(opac_renewal,false),
2443                 false
2444             ) FROM action.circulation WHERE id = $1
2445         )
2446     );
2447 $$ LANGUAGE SQL;