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