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