moving marc manipulation to it's own file
[migration-tools.git] / sql / base / base.sql
index 603cffe..56cab3d 100644 (file)
@@ -244,7 +244,7 @@ CREATE OR REPLACE FUNCTION migration_tools.build_specific_base_staging_table (TE
     BEGIN
         base_staging_table = REPLACE( production_table, '.', '_' );
         --RAISE INFO 'In migration_tools.build_specific_base_staging_table(%,%) -> %', migration_schema, production_table, base_staging_table;
-        PERFORM migration_tools.exec( $1, 'CREATE UNLOGGED TABLE ' || migration_schema || '.' || base_staging_table || ' ( LIKE ' || production_table || ' INCLUDING DEFAULTS EXCLUDING CONSTRAINTS );' );
+        PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.' || base_staging_table || ' ( LIKE ' || production_table || ' INCLUDING DEFAULTS EXCLUDING CONSTRAINTS );' );
         PERFORM migration_tools.exec( $1, '
             INSERT INTO ' || migration_schema || '.fields_requiring_mapping
                 SELECT table_schema, table_name, column_name, data_type
@@ -261,6 +261,36 @@ CREATE OR REPLACE FUNCTION migration_tools.build_specific_base_staging_table (TE
     END;
 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
 
+-- creates other child table so you can have more than one child table in a schema from a base table 
+CREATE OR REPLACE FUNCTION build_variant_staging_table(text, text, text)
+ RETURNS void
+ LANGUAGE plpgsql
+ STRICT
+AS $function$
+    DECLARE
+        migration_schema ALIAS FOR $1;
+        production_table ALIAS FOR $2;
+        base_staging_table ALIAS FOR $3;
+        columns RECORD;
+    BEGIN
+        --RAISE INFO 'In migration_tools.build_specific_base_staging_table(%,%) -> %', migration_schema, production_table, base_staging_table;
+        PERFORM migration_tools.exec( $1, 'CREATE TABLE ' || migration_schema || '.' || base_staging_table || ' ( LIKE ' || production_table || ' INCLUDING DEFAULTS EXCLUDING CONSTRAINTS );' );
+        PERFORM migration_tools.exec( $1, '
+            INSERT INTO ' || migration_schema || '.fields_requiring_mapping
+                SELECT table_schema, table_name, column_name, data_type
+                FROM information_schema.columns
+                WHERE table_schema = ''' || migration_schema || ''' AND table_name = ''' || base_staging_table || ''' AND is_nullable = ''NO'' AND column_default IS NULL;
+        ' );
+        FOR columns IN
+            SELECT table_schema, table_name, column_name, data_type
+            FROM information_schema.columns
+            WHERE table_schema = migration_schema AND table_name = base_staging_table AND is_nullable = 'NO' AND column_default IS NULL
+        LOOP
+            PERFORM migration_tools.exec( $1, 'ALTER TABLE ' || columns.table_schema || '.' || columns.table_name || ' ALTER COLUMN ' || columns.column_name || ' DROP NOT NULL;' );
+        END LOOP;
+    END;
+$function$
+
 CREATE OR REPLACE FUNCTION migration_tools.create_linked_legacy_table_from (TEXT,TEXT,TEXT) RETURNS VOID AS $$
     DECLARE
         migration_schema ALIAS FOR $1;
@@ -272,7 +302,7 @@ CREATE OR REPLACE FUNCTION migration_tools.create_linked_legacy_table_from (TEXT
         column_list TEXT := '';
         column_count INTEGER := 0;
     BEGIN
-        create_sql := 'CREATE UNLOGGED TABLE ' || migration_schema || '.' || parent_table || '_legacy ( ';
+        create_sql := 'CREATE TABLE ' || migration_schema || '.' || parent_table || '_legacy ( ';
         FOR columns IN
             SELECT table_schema, table_name, column_name, data_type
             FROM information_schema.columns
@@ -573,6 +603,98 @@ CREATE OR REPLACE FUNCTION migration_tools.name_parse_out_fuller_last_first_midd
     END;
 $$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
 
+CREATE OR REPLACE FUNCTION migration_tools.name_parse_out_fuller_last_first_middle_and_random_affix2 (TEXT) RETURNS TEXT[] AS $$
+    DECLARE
+        full_name TEXT := $1;
+        temp TEXT;
+        family_name TEXT := '';
+        first_given_name TEXT := '';
+        second_given_name TEXT := '';
+        suffix TEXT := '';
+        prefix TEXT := '';
+    BEGIN
+        temp := BTRIM(full_name);
+        -- Use values, not structure, for prefix/suffix, unless we come up with a better idea
+        --IF temp ~ '^\S{2,}\.' THEN
+        --    prefix := REGEXP_REPLACE(temp, '^(\S{2,}\.).*$','\1');
+        --    temp := BTRIM(REGEXP_REPLACE(temp, '^\S{2,}\.(.*)$','\1'));
+        --END IF;
+        --IF temp ~ '\S{2,}\.$' THEN
+        --    suffix := REGEXP_REPLACE(temp, '^.*(\S{2,}\.)$','\1');
+        --    temp := REGEXP_REPLACE(temp, '^(.*)\S{2,}\.$','\1');
+        --END IF;
+        IF temp ilike '%MR.%' THEN
+            prefix := 'Mr.';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'MR\.\\s*', '', 'i' ));
+        END IF;
+        IF temp ilike '%MRS.%' THEN
+            prefix := 'Mrs.';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'MRS\.\\s*', '', 'i' ));
+        END IF;
+        IF temp ilike '%MS.%' THEN
+            prefix := 'Ms.';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'MS\.\\s*', '', 'i' ));
+        END IF;
+        IF temp ilike '%DR.%' THEN
+            prefix := 'Dr.';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'DR\.\\s*', '', 'i' ));
+        END IF;
+        IF temp ilike '%JR.%' THEN
+            suffix := 'Jr.';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'JR\.\\s*', '', 'i' ));
+        END IF;
+        IF temp ilike '%JR,%' THEN
+            suffix := 'Jr.';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'JR,\\s*', ',', 'i' ));
+        END IF;
+        IF temp ilike '%SR.%' THEN
+            suffix := 'Sr.';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'SR\.\\s*', '', 'i' ));
+        END IF;
+        IF temp ilike '%SR,%' THEN
+            suffix := 'Sr.';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'SR,\\s*', ',', 'i' ));
+        END IF;
+        IF temp like '%III%' THEN
+            suffix := 'III';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'III', '' ));
+        END IF;
+        IF temp like '%II%' THEN
+            suffix := 'II';
+            temp := BTRIM(REGEXP_REPLACE( temp, E'II', '' ));
+        END IF;
+
+        IF temp ~ ',' THEN
+            family_name = BTRIM(REGEXP_REPLACE(temp,'^(.*?,).*$','\1'));
+            temp := BTRIM(REPLACE( temp, family_name, '' ));
+            family_name := REPLACE( family_name, ',', '' );
+            IF temp ~ ' ' THEN
+                first_given_name := BTRIM( REGEXP_REPLACE(temp,'^(.+)\s(.+)$','\1') );
+                second_given_name := BTRIM( REGEXP_REPLACE(temp,'^(.+)\s(.+)$','\2') );
+            ELSE
+                first_given_name := temp;
+                second_given_name := '';
+            END IF;
+        ELSE
+            IF temp ~ '^\S+\s+\S+\s+\S+$' THEN
+                first_given_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)\s*(\S+)$','\1') );
+                second_given_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)\s*(\S+)$','\2') );
+                family_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)\s*(\S+)$','\3') );
+            ELSE
+                first_given_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)$','\1') );
+                second_given_name := temp;
+                family_name := BTRIM( REGEXP_REPLACE(temp,'^(\S+)\s*(\S+)$','\2') );
+            END IF;
+        END IF;
+
+        family_name := BTRIM(REPLACE(REPLACE(family_name,',',''),'"',''));
+        first_given_name := BTRIM(REPLACE(REPLACE(first_given_name,',',''),'"',''));
+        second_given_name := BTRIM(REPLACE(REPLACE(second_given_name,',',''),'"',''));
+
+        RETURN ARRAY[ family_name, prefix, first_given_name, second_given_name, suffix ];
+    END;
+$$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
+
 CREATE OR REPLACE FUNCTION migration_tools.address_parse_out_citystatezip (TEXT) RETURNS TEXT[] AS $$
     DECLARE
         city_state_zip TEXT := $1;
@@ -688,7 +810,7 @@ CREATE OR REPLACE FUNCTION migration_tools.parse_out_address2 (TEXT) RETURNS TEX
 $$ LANGUAGE PLPERLU STABLE;
 
 DROP TABLE IF EXISTS migration_tools.usps_suffixes;
-CREATE UNLOGGED TABLE migration_tools.usps_suffixes ( suffix_from TEXT, suffix_to TEXT );
+CREATE TABLE migration_tools.usps_suffixes ( suffix_from TEXT, suffix_to TEXT );
 INSERT INTO migration_tools.usps_suffixes VALUES
     ('ALLEE','ALY'),
     ('ALLEY','ALY'),
@@ -1583,6 +1705,241 @@ CREATE OR REPLACE FUNCTION migration_tools.attempt_sierra_timestamp (TEXT,TEXT)
     END;
 $$ LANGUAGE PLPGSQL STRICT STABLE;
 
+CREATE OR REPLACE FUNCTION migration_tools.openbiblio2marc (x_bibid TEXT) RETURNS TEXT AS $func$
+BEGIN
+-- Expects the following table/columns:
+
+-- export_biblio_tsv:
+-- l_bibid               | 1
+-- l_create_dt           | 2007-03-07 09:03:09
+-- l_last_change_dt      | 2015-01-23 11:18:54
+-- l_last_change_userid  | 2
+-- l_material_cd         | 10
+-- l_collection_cd       | 13
+-- l_call_nmbr1          | Canada
+-- l_call_nmbr2          | ON
+-- l_call_nmbr3          | Ottawa 18
+-- l_title               | Art and the courts : France ad England
+-- l_title_remainder     | from 1259-1328
+-- l_responsibility_stmt |
+-- l_author              | National Gallery of Canada
+-- l_topic1              |
+-- l_topic2              |
+-- l_topic3              |
+-- l_topic4              |
+-- l_topic5              |
+-- l_opac_flg            | Y
+-- l_flag_attention      | 0
+
+-- export_biblio_field_tsv:
+-- l_bibid       | 1
+-- l_fieldid     | 1
+-- l_tag         | 720
+-- l_ind1_cd     | N
+-- l_ind2_cd     | N
+-- l_subfield_cd | a
+-- l_field_data  | Brieger, Peter Henry
+
+-- Map export_biblio_tsv as follows:
+-- l_call_nmbr?             -> 099a
+-- l_author                 -> 100a
+-- l_title                  -> 245a
+-- l_title_remainder        -> 245b
+-- l_responsibility_stmt    -> 245c
+-- l_topic?                 -> 650a
+-- l_bibid                  -> 001
+
+RETURN
+    migration_tools.consolidate_tag( migration_tools.make_stub_bib(y.tag,y.ind1,y.ind2,y.data), '245' )
+FROM (
+    select
+        array_agg(lpad(l_tag,3,'0') || l_subfield_cd) as "tag",
+        array_agg(l_ind1_cd) as "ind1",
+        array_agg(l_ind2_cd) as "ind2",
+        array_agg(l_field_data) as "data"
+    from (
+        select
+            l_tag,
+            l_subfield_cd,
+            l_ind1_cd,
+            l_ind2_cd,
+            l_field_data
+        from export_biblio_field_tsv
+        where l_bibid = x_bibid
+    union
+        select
+            '099' as "l_tag",
+            'a' as "l_subfield_cd",
+            ' ' as "l_ind1_cd",
+            ' ' as "l_ind2_cd",
+            concat_ws(' ',
+                nullif(btrim(l_call_nmbr1),''),
+                nullif(btrim(l_call_nmbr2),''),
+                nullif(btrim(l_call_nmbr3),'')
+            ) as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid
+    union
+        select
+            '100' as "l_tag",
+            'a' as "l_subfield_cd",
+            ' ' as "l_ind1_cd",
+            ' ' as "l_ind2_cd",
+            l_author as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid and nullif(btrim(l_author),'') is not null
+    union
+        select
+            '245' as "l_tag",
+            'a' as "l_subfield_cd",
+            ' ' as "l_ind1_cd",
+            ' ' as "l_ind2_cd",
+            l_title as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid and nullif(btrim(l_title),'') is not null
+    union
+        select
+            '245' as "l_tag",
+            'b' as "l_subfield_cd",
+            ' ' as "l_ind1_cd",
+            ' ' as "l_ind2_cd",
+            l_title_remainder as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid and nullif(btrim(l_title_remainder),'') is not null
+    union
+        select
+            '650' as "l_tag",
+            'a' as "l_subfield_cd",
+            ' ' as "l_ind1_cd",
+            ' ' as "l_ind2_cd",
+            l_topic1 as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid and nullif(btrim(l_topic1),'') is not null
+    union
+        select
+            '650' as "l_tag",
+            'a' as "l_subfield_cd",
+            ' ' as "l_ind1_cd",
+            ' ' as "l_ind2_cd",
+            l_topic2 as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid and nullif(btrim(l_topic2),'') is not null
+    union
+        select
+            '650' as "l_tag",
+            'a' as "l_subfield_cd",
+            ' ' as "l_ind1_cd",
+            ' ' as "l_ind2_cd",
+            l_topic3 as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid and nullif(btrim(l_topic3),'') is not null
+    union
+        select
+            '650' as "l_tag",
+            'a' as "l_subfield_cd",
+            ' ' as "l_ind1_cd",
+            ' ' as "l_ind2_cd",
+            l_topic4 as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid and nullif(btrim(l_topic4),'') is not null
+    union
+        select
+            '650' as "l_tag",
+            'a' as "l_subfield_cd",
+            ' ' as "l_ind1_cd",
+            ' ' as "l_ind2_cd",
+            l_topic5 as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid and nullif(btrim(l_topic5),'') is not null
+    union
+        select
+            '001' as "l_tag",
+            '' as "l_subfield_cd",
+            '' as "l_ind1_cd",
+            '' as "l_ind2_cd",
+            l_bibid as "l_field_data"
+        from export_biblio_tsv
+        where l_bibid = x_bibid
+    ) x
+) y;
+
+END
+$func$ LANGUAGE plpgsql;
+
+-- add koha holding tag to marc
+DROP FUNCTION IF EXISTS migration_tools.generate_koha_holding_tag(TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT);
+
+CREATE OR REPLACE FUNCTION migration_tools.generate_koha_holding_tag(marc TEXT, tag TEXT, ind1 TEXT, ind2 TEXT, barcode TEXT, dateaccessioned TEXT, booksellerid TEXT, homebranch TEXT, price TEXT, replacementprice TEXT, replacementpricedate TEXT, datelastborrowed TEXT, datelastseen TEXT, stack TEXT, notforloan TEXT, damaged TEXT, itemlost TEXT, wthdrawn TEXT, itemcallnumber TEXT, issues TEXT, renewals TEXT, reserves TEXT, restricted TEXT, internalnotes TEXT, itemnotes TEXT, holdingbranch TEXT, location TEXT, onloan TEXT, cn_source TEXT, cn_sort TEXT, ccode TEXT, materials TEXT, uri TEXT, itype TEXT, enumchron TEXT, copynumber TEXT, stocknumber TEXT)
+ RETURNS TEXT
+ LANGUAGE plperlu
+AS $function$
+use strict;
+use warnings;
+
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'utf8');
+
+binmode(STDERR, ':bytes');
+binmode(STDOUT, ':utf8');
+binmode(STDERR, ':utf8');
+
+my ($marc_xml, $tag , $ind1 , $ind2 , $barcode , $dateaccessioned , $booksellerid , $homebranch , $price , $replacementprice , $replacementpricedate , $datelastborrowed , $datelastseen , $stack , $notforloan , $damaged , $itemlost , $wthdrawn , $itemcallnumber , $issues , $renewals , $reserves , $restricted , $internalnotes , $itemnotes , $holdingbranch , $location , $onloan , $cn_source , $cn_sort , $ccode , $materials , $uri , $itype , $enumchron , $copynumber , $stocknumber ) = @_;
+
+$marc_xml =~ s/(<leader>.........)./${1}a/;
+
+eval {
+    $marc_xml = MARC::Record->new_from_xml($marc_xml);
+};
+if ($@) {
+    #elog("could not parse $bibid: $@\n");
+    import MARC::File::XML (BinaryEncoding => 'utf8');
+    return $marc_xml;
+}
+
+my $new_field = new MARC::Field(
+    $tag, $ind1, $ind2,
+    'a' => $homebranch,
+    'b' => $holdingbranch,
+    'c' => $location,
+    'p' => $barcode,
+    'y' => $itype
+);
+
+if ($dateaccessioned) { $new_field->add_subfields('d' => $dateaccessioned); }
+if ($booksellerid) { $new_field->add_subfields('e' => $booksellerid); }
+if ($price) { $new_field->add_subfields('g' => $price); }
+if ($replacementprice) { $new_field->add_subfields('v' => $replacementprice); }
+if ($replacementpricedate) { $new_field->add_subfields('w' => $replacementpricedate); }
+if ($datelastborrowed) { $new_field->add_subfields('s' => $datelastborrowed); }
+if ($datelastseen) { $new_field->add_subfields('r' => $datelastseen); }
+if ($stack) { $new_field->add_subfields('j' => $stack); }
+if ($notforloan) { $new_field->add_subfields('7' => $notforloan); }
+if ($damaged) { $new_field->add_subfields('4' => $damaged); }
+if ($itemlost) { $new_field->add_subfields('1' => $itemlost); }
+if ($wthdrawn) { $new_field->add_subfields('0' => $wthdrawn); }
+if ($itemcallnumber) { $new_field->add_subfields('o' => $itemcallnumber); }
+if ($issues) { $new_field->add_subfields('l' => $issues); }
+if ($renewals) { $new_field->add_subfields('m' => $renewals); }
+if ($reserves) { $new_field->add_subfields('n' => $reserves); }
+if ($restricted) { $new_field->add_subfields('5' => $restricted); }
+if ($internalnotes) { $new_field->add_subfields('x' => $internalnotes); }
+if ($itemnotes) { $new_field->add_subfields('z' => $itemnotes); }
+if ($onloan) { $new_field->add_subfields('q' => $onloan); }
+if ($cn_source) { $new_field->add_subfields('2' => $cn_source); }
+if ($cn_sort) { $new_field->add_subfields('6' => $cn_sort); }
+if ($ccode) { $new_field->add_subfields('8' => $ccode); }
+if ($materials) { $new_field->add_subfields('3' => $materials); }
+if ($uri) { $new_field->add_subfields('u' => $uri); }
+if ($enumchron) { $new_field->add_subfields('h' => $enumchron); }
+if ($copynumber) { $new_field->add_subfields('t' => $copynumber); }
+if ($stocknumber) { $new_field->add_subfields('i' => $stocknumber); }
+
+$marc_xml->insert_grouped_field( $new_field );
+
+return $marc_xml->as_xml_record();
+
+$function$;
+
 CREATE OR REPLACE FUNCTION migration_tools.attempt_money (TEXT,TEXT) RETURNS NUMERIC(8,2) AS $$
     DECLARE
         attempt_value ALIAS FOR $1;
@@ -2842,29 +3199,6 @@ END;
 
 $$ LANGUAGE plpgsql;
 
-CREATE OR REPLACE FUNCTION migration_tools.marc_parses( TEXT ) RETURNS BOOLEAN AS $func$
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'UTF-8');
-use MARC::Charset;
-
-MARC::Charset->assume_unicode(1);
-
-my $xml = shift;
-
-eval {
-    my $r = MARC::Record->new_from_xml( $xml );
-    my $output_xml = $r->as_xml_record();
-};
-if ($@) {
-    return 0;
-} else {
-    return 1;
-}
-
-$func$ LANGUAGE PLPERLU;
-COMMENT ON FUNCTION migration_tools.marc_parses(TEXT) IS 'Return boolean indicating if MARCXML string is parseable by MARC::File::XML';
-
 CREATE OR REPLACE FUNCTION migration_tools.simple_export_library_config(dir TEXT, orgs INT[]) RETURNS VOID AS $FUNC$
 BEGIN
    EXECUTE $$COPY (SELECT * FROM actor.hours_of_operation WHERE id IN ($$ ||
@@ -2964,172 +3298,6 @@ BEGIN
 END;
 $FUNC$ LANGUAGE PLPGSQL;
 
-CREATE OR REPLACE FUNCTION migration_tools.merge_marc_fields( TEXT, TEXT, TEXT[] ) RETURNS TEXT AS $func$
-
-use strict;
-use warnings;
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'UTF-8');
-use MARC::Charset;
-
-MARC::Charset->assume_unicode(1);
-
-my $target_xml = shift;
-my $source_xml = shift;
-my $tags = shift;
-
-my $target;
-my $source;
-
-eval { $target = MARC::Record->new_from_xml( $target_xml ); };
-if ($@) {
-    return;
-}
-eval { $source = MARC::Record->new_from_xml( $source_xml ); };
-if ($@) {
-    return;
-}
-
-my $source_id = $source->subfield('901', 'c');
-$source_id = $source->subfield('903', 'a') unless $source_id;
-my $target_id = $target->subfield('901', 'c');
-$target_id = $target->subfield('903', 'a') unless $target_id;
-
-my %existing_fields;
-foreach my $tag (@$tags) {
-    my %existing_fields = map { $_->as_formatted() => 1 } $target->field($tag);
-    my @to_add = grep { not exists $existing_fields{$_->as_formatted()} } $source->field($tag);
-    $target->insert_fields_ordered(map { $_->clone() } @to_add);
-    if (@to_add) {
-        elog(NOTICE, "Merged $tag tag(s) from $source_id to $target_id");
-    }
-}
-
-my $xml = $target->as_xml_record;
-$xml =~ s/^<\?.+?\?>$//mo;
-$xml =~ s/\n//sgo;
-$xml =~ s/>\s+</></sgo;
-
-return $xml;
-
-$func$ LANGUAGE PLPERLU;
-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.';
-
-CREATE OR REPLACE FUNCTION migration_tools.make_stub_bib (text[], text[]) RETURNS TEXT AS $func$
-
-use strict;
-use warnings;
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'UTF-8');
-use Text::CSV;
-
-my $in_tags = shift;
-my $in_values = shift;
-
-# hack-and-slash parsing of array-passed-as-string;
-# this can go away once everybody is running Postgres 9.1+
-my $csv = Text::CSV->new({binary => 1});
-$in_tags =~ s/^{//;
-$in_tags =~ s/}$//;
-my $status = $csv->parse($in_tags);
-my $tags = [ $csv->fields() ];
-$in_values =~ s/^{//;
-$in_values =~ s/}$//;
-$status = $csv->parse($in_values);
-my $values = [ $csv->fields() ];
-
-my $marc = MARC::Record->new();
-
-$marc->leader('00000nam a22000007  4500');
-$marc->append_fields(MARC::Field->new('008', '000000s                       000   eng d'));
-
-foreach my $i (0..$#$tags) {
-    my ($tag, $sf);
-    if ($tags->[$i] =~ /^(\d{3})([0-9a-z])$/) {
-        $tag = $1;
-        $sf = $2;
-        $marc->append_fields(MARC::Field->new($tag, ' ', ' ', $sf => $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
-    } elsif ($tags->[$i] =~ /^(\d{3})$/) {
-        $tag = $1;
-        $marc->append_fields(MARC::Field->new($tag, $values->[$i])) if $values->[$i] !~ /^\s*$/ and $values->[$i] ne 'NULL';
-    }
-}
-
-my $xml = $marc->as_xml_record;
-$xml =~ s/^<\?.+?\?>$//mo;
-$xml =~ s/\n//sgo;
-$xml =~ s/>\s+</></sgo;
-
-return $xml;
-
-$func$ LANGUAGE PLPERLU;
-COMMENT ON FUNCTION migration_tools.make_stub_bib (text[], text[]) IS $$Simple function to create a stub MARCXML bib from a set of columns.
-The first argument is an array of tag/subfield specifiers, e.g., ARRAY['001', '245a', '500a'].
-The second argument is an array of text containing the values to plug into each field.  
-If the value for a given field is NULL or the empty string, it is not inserted.
-$$;
-
-CREATE OR REPLACE FUNCTION migration_tools.set_indicator (TEXT, TEXT, INTEGER, CHAR(1)) RETURNS TEXT AS $func$
-
-my ($marcxml, $tag, $pos, $value) = @_;
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'UTF-8');
-use MARC::Charset;
-use strict;
-
-MARC::Charset->assume_unicode(1);
-
-elog(ERROR, 'indicator position must be either 1 or 2') unless $pos =~ /^[12]$/;
-elog(ERROR, 'MARC tag must be numeric') unless $tag =~ /^\d{3}$/;
-elog(ERROR, 'MARC tag must not be control field') if $tag =~ /^00/;
-elog(ERROR, 'Value must be exactly one character') unless $value =~ /^.$/;
-
-my $xml = $marcxml;
-eval {
-    my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
-
-    foreach my $field ($marc->field($tag)) {
-        $field->update("ind$pos" => $value);
-    }
-    $xml = $marc->as_xml_record;
-    $xml =~ s/^<\?.+?\?>$//mo;
-    $xml =~ s/\n//sgo;
-    $xml =~ s/>\s+</></sgo;
-};
-return $xml;
-
-$func$ LANGUAGE PLPERLU;
-
-COMMENT ON FUNCTION migration_tools.set_indicator(TEXT, TEXT, INTEGER, CHAR(1)) IS $$Set indicator value of a specified MARC field.
-The first argument is a MARCXML string.
-The second argument is a MARC tag.
-The third argument is the indicator position, either 1 or 2.
-The fourth argument is the character to set the indicator value to.
-All occurences of the specified field will be changed.
-The function returns the revised MARCXML string.$$;
-
-CREATE OR REPLACE FUNCTION migration_tools.create_staff_user(
-    username TEXT,
-    password TEXT,
-    org TEXT,
-    perm_group TEXT,
-    first_name TEXT DEFAULT '',
-    last_name TEXT DEFAULT ''
-) RETURNS VOID AS $func$
-BEGIN
-    RAISE NOTICE '%', org ;
-    INSERT INTO actor.usr (usrname, passwd, ident_type, first_given_name, family_name, home_ou, profile)
-    SELECT username, password, 1, first_name, last_name, aou.id, pgt.id
-    FROM   actor.org_unit aou, permission.grp_tree pgt
-    WHERE  aou.shortname = org
-    AND    pgt.name = perm_group;
-END
-$func$
-LANGUAGE PLPGSQL;
-
 -- example: SELECT * FROM migration_tools.duplicate_template(5,'{3,4}');
 CREATE OR REPLACE FUNCTION migration_tools.duplicate_template (INTEGER, INTEGER[]) RETURNS VOID AS $$
     DECLARE
@@ -3402,55 +3570,6 @@ CREATE OR REPLACE FUNCTION migration_tools.reset_event (BIGINT) RETURNS VOID AS
         id = $1;
 $$ LANGUAGE SQL;
 
-CREATE OR REPLACE FUNCTION migration_tools.get_marc_leader (TEXT) RETURNS TEXT AS $$
-    my ($marcxml) = @_;
-
-    use MARC::Record;
-    use MARC::File::XML;
-    use MARC::Field;
-
-    my $field;
-    eval {
-        my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
-        $field = $marc->leader();
-    };
-    return $field;
-$$ LANGUAGE PLPERLU STABLE;
-
-CREATE OR REPLACE FUNCTION migration_tools.get_marc_tag (TEXT, TEXT, TEXT, TEXT) RETURNS TEXT AS $$
-    my ($marcxml, $tag, $subfield, $delimiter) = @_;
-
-    use MARC::Record;
-    use MARC::File::XML;
-    use MARC::Field;
-
-    my $field;
-    eval {
-        my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
-        $field = $marc->field($tag);
-    };
-    return $field->as_string($subfield,$delimiter);
-$$ LANGUAGE PLPERLU STABLE;
-
-CREATE OR REPLACE FUNCTION migration_tools.get_marc_tags (TEXT, TEXT, TEXT, TEXT) RETURNS TEXT[] AS $$
-    my ($marcxml, $tag, $subfield, $delimiter) = @_;
-
-    use MARC::Record;
-    use MARC::File::XML;
-    use MARC::Field;
-
-    my @fields;
-    eval {
-        my $marc = MARC::Record->new_from_xml($marcxml, 'UTF-8');
-        @fields = $marc->field($tag);
-    };
-    my @texts;
-    foreach my $field (@fields) {
-        push @texts, $field->as_string($subfield,$delimiter);
-    }
-    return \@texts;
-$$ LANGUAGE PLPERLU STABLE;
-
 CREATE OR REPLACE FUNCTION migration_tools.find_hold_matrix_matchpoint (INTEGER) RETURNS INTEGER AS $$
     SELECT action.find_hold_matrix_matchpoint(
         (SELECT pickup_lib FROM action.hold_request WHERE id = $1),
@@ -3558,13 +3677,17 @@ $$ LANGUAGE PLPGSQL STRICT VOLATILE;
 
 
 -- convenience functions for handling copy_location maps
-
 CREATE OR REPLACE FUNCTION migration_tools.handle_shelf (TEXT,TEXT,TEXT,INTEGER) RETURNS VOID AS $$
+    SELECT migration_tools._handle_shelf($1,$2,$3,$4,TRUE);
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION migration_tools._handle_shelf (TEXT,TEXT,TEXT,INTEGER,BOOLEAN) RETURNS VOID AS $$
     DECLARE
         table_schema ALIAS FOR $1;
         table_name ALIAS FOR $2;
         org_shortname ALIAS FOR $3;
         org_range ALIAS FOR $4;
+        make_assertion ALIAS FOR $5;
         proceed BOOLEAN;
         org INTEGER;
         -- if x_org is on the mapping table, it'll take precedence over the passed org_shortname param
@@ -3573,6 +3696,7 @@ CREATE OR REPLACE FUNCTION migration_tools.handle_shelf (TEXT,TEXT,TEXT,INTEGER)
         x_org INTEGER;
         org_list INTEGER[];
         o INTEGER;
+        row_count NUMERIC;
     BEGIN
         EXECUTE 'SELECT EXISTS (
             SELECT 1
@@ -3608,26 +3732,28 @@ CREATE OR REPLACE FUNCTION migration_tools.handle_shelf (TEXT,TEXT,TEXT,INTEGER)
             || ' ADD COLUMN x_shelf INTEGER';
 
         IF x_org_found THEN
+            RAISE INFO 'Found x_org column';
             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
-                || ' SET x_shelf = id FROM asset_copy_location b'
+                || ' SET x_shelf = b.id FROM asset_copy_location b'
                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
                 || ' AND b.owning_lib = x_org'
                 || ' AND NOT b.deleted';
             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
-                || ' SET x_shelf = id FROM asset.copy_location b'
+                || ' SET x_shelf = b.id FROM asset.copy_location b'
                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
                 || ' AND b.owning_lib = x_org'
                 || ' AND x_shelf IS NULL'
                 || ' AND NOT b.deleted';
         ELSE
+            RAISE INFO 'Did not find x_org column';
             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
-                || ' SET x_shelf = id FROM asset_copy_location b'
+                || ' SET x_shelf = b.id FROM asset_copy_location b'
                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
                 || ' AND b.owning_lib = $1'
                 || ' AND NOT b.deleted'
             USING org;
             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
-                || ' SET x_shelf = id FROM asset_copy_location b'
+                || ' SET x_shelf = b.id FROM asset_copy_location b'
                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
                 || ' AND b.owning_lib = $1'
                 || ' AND x_shelf IS NULL'
@@ -3636,19 +3762,24 @@ CREATE OR REPLACE FUNCTION migration_tools.handle_shelf (TEXT,TEXT,TEXT,INTEGER)
         END IF;
 
         FOREACH o IN ARRAY org_list LOOP
+            RAISE INFO 'Considering org %', o;
             EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
-                || ' SET x_shelf = id FROM asset.copy_location b'
+                || ' SET x_shelf = b.id FROM asset.copy_location b'
                 || ' WHERE BTRIM(UPPER(a.desired_shelf)) = BTRIM(UPPER(b.name))'
                 || ' AND b.owning_lib = $1 AND x_shelf IS NULL'
                 || ' AND NOT b.deleted'
             USING o;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            RAISE INFO 'Updated % rows', row_count;
         END LOOP;
 
-        EXECUTE 'SELECT migration_tools.assert(
-            NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_shelf <> '''' AND x_shelf IS NULL),
-            ''Cannot find a desired location'',
-            ''Found all desired locations''
-        );';
+        IF make_assertion THEN
+            EXECUTE 'SELECT migration_tools.assert(
+                NOT EXISTS (SELECT 1 FROM ' || quote_ident(table_name) || ' WHERE desired_shelf <> '''' AND x_shelf IS NULL),
+                ''Cannot find a desired location'',
+                ''Found all desired locations''
+            );';
+        END IF;
 
     END;
 $$ LANGUAGE PLPGSQL STRICT VOLATILE;
@@ -3768,7 +3899,7 @@ CREATE OR REPLACE FUNCTION migration_tools.handle_org (TEXT,TEXT) RETURNS VOID A
             || ' ADD COLUMN x_org INTEGER';
 
         EXECUTE 'UPDATE ' || quote_ident(table_name) || ' a'
-            || ' SET x_org = id FROM actor.org_unit b'
+            || ' SET x_org = b.id FROM actor.org_unit b'
             || ' WHERE BTRIM(a.desired_org) = BTRIM(b.shortname)';
 
         EXECUTE 'SELECT migration_tools.assert(
@@ -4182,216 +4313,7 @@ BEGIN
 END
 $$ LANGUAGE plpgsql;
 
--- yet another subfield 9 function, this one only adds the $9 if the ind1 = 1 or 4 and ind2 = 0 or 1
-DROP FUNCTION IF EXISTS migration_tools.strict_add_sf9(TEXT,TEXT);
-CREATE OR REPLACE FUNCTION migration_tools.strict_add_sf9(marc TEXT, new_9 TEXT)
- RETURNS TEXT
- LANGUAGE plperlu
-AS $function$
-use strict;
-use warnings;
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'utf8');
-
-binmode(STDERR, ':bytes');
-binmode(STDOUT, ':utf8');
-binmode(STDERR, ':utf8');
-
-my $marc_xml = shift;
-my $new_9_to_set = shift;
-
-$marc_xml =~ s/(<leader>.........)./${1}a/;
-
-eval {
-    $marc_xml = MARC::Record->new_from_xml($marc_xml);
-};
-if ($@) {
-    #elog("could not parse $bibid: $@\n");
-    import MARC::File::XML (BinaryEncoding => 'utf8');
-    return $marc_xml;
-}
-
-my @uris = $marc_xml->field('856');
-return $marc_xml->as_xml_record() unless @uris;
-
-foreach my $field (@uris) {
-    my $ind1 = $field->indicator('1');
-    if (!defined $ind1) { next; }
-    if ($ind1 ne '1' && $ind1 ne '4') { next; }
-    my $ind2 = $field->indicator('2');
-    if (!defined $ind2) { next; }
-    if ($ind2 ne '0' && $ind2 ne '1') { next; }
-    $field->add_subfields( '9' => $new_9_to_set );
-}
-
-return $marc_xml->as_xml_record();
-
-$function$;
-
--- yet another subfield 9 function, this one only adds the $9 and forces
--- ind1 = 4 if not already ind1 = 1 or 4 and ind2 = 0 if not already ind2 = 0 or 1
-DROP FUNCTION IF EXISTS migration_tools.force_add_sf9(TEXT,TEXT);
-CREATE OR REPLACE FUNCTION migration_tools.force_add_sf9(marc TEXT, new_9 TEXT)
- RETURNS TEXT
- LANGUAGE plperlu
-AS $function$
-use strict;
-use warnings;
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'utf8');
-
-binmode(STDERR, ':bytes');
-binmode(STDOUT, ':utf8');
-binmode(STDERR, ':utf8');
-
-my $marc_xml = shift;
-my $new_9_to_set = shift;
-
-$marc_xml =~ s/(<leader>.........)./${1}a/;
-
-eval {
-    $marc_xml = MARC::Record->new_from_xml($marc_xml);
-};
-if ($@) {
-    #elog("could not parse $bibid: $@\n");
-    import MARC::File::XML (BinaryEncoding => 'utf8');
-    return $marc_xml;
-}
-
-my @uris = $marc_xml->field('856');
-return $marc_xml->as_xml_record() unless @uris;
-
-foreach my $field (@uris) {
-    my $ind1 = $field->indicator('1');
-    if (!defined $ind1) { next; }
-    if ($ind1 ne '1' && $ind1 ne '4') { $field->set_indicator(1,'4'); }
-    my $ind2 = $field->indicator('2');
-    if (!defined $ind2) { next; }
-    if ($ind2 ne '0' && $ind2 ne '1') { $field->set_indicator(2,'0'); }
-    $field->add_subfields( '9' => $new_9_to_set );
-}
-
-return $marc_xml->as_xml_record();
-
-$function$;
-
--- alternate adding subfield 9 function in that it adds them to existing tags where the 856$u matches a correct value only
-DROP FUNCTION IF EXISTS migration_tools.add_sf9(TEXT,TEXT,TEXT);
-CREATE OR REPLACE FUNCTION migration_tools.add_sf9(marc TEXT, partial_u TEXT, new_9 TEXT)
- RETURNS TEXT
- LANGUAGE plperlu
-AS $function$
-use strict;
-use warnings;
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'utf8');
-
-binmode(STDERR, ':bytes');
-binmode(STDOUT, ':utf8');
-binmode(STDERR, ':utf8');
-
-my $marc_xml = shift;
-my $matching_u_text = shift;
-my $new_9_to_set = shift;
-
-$marc_xml =~ s/(<leader>.........)./${1}a/;
-
-eval {
-    $marc_xml = MARC::Record->new_from_xml($marc_xml);
-};
-if ($@) {
-    #elog("could not parse $bibid: $@\n");
-    import MARC::File::XML (BinaryEncoding => 'utf8');
-    return;
-}
-
-my @uris = $marc_xml->field('856');
-return unless @uris;
-
-foreach my $field (@uris) {
-    my $sfu = $field->subfield('u');
-    my $ind2 = $field->indicator('2');
-    if (!defined $ind2) { next; }
-    if ($ind2 ne '0') { next; }
-    if (!defined $sfu) { next; }
-    if ($sfu =~ m/$matching_u_text/ or $matching_u_text eq 'pineapple') {
-        $field->add_subfields( '9' => $new_9_to_set );
-        last;
-    }
-}
-
-return $marc_xml->as_xml_record();
-
-$function$;
-
-DROP FUNCTION IF EXISTS migration_tools.add_sf9(BIGINT, TEXT, TEXT, REGCLASS);
-CREATE OR REPLACE FUNCTION migration_tools.add_sf9(bib_id BIGINT, target_u_text TEXT, sf9_text TEXT, bib_table REGCLASS)
-    RETURNS BOOLEAN AS
-$BODY$
-DECLARE
-    source_xml    TEXT;
-    new_xml       TEXT;
-    r             BOOLEAN;
-BEGIN
-
-    EXECUTE 'SELECT marc FROM ' || bib_table || ' WHERE id = ' || bib_id INTO source_xml;
-
-    SELECT migration_tools.add_sf9(source_xml, target_u_text, sf9_text) INTO new_xml;
-
-    r = FALSE;
-       new_xml = '$_$' || new_xml || '$_$';
-
-    IF new_xml != source_xml THEN
-        EXECUTE 'UPDATE ' || bib_table || ' SET marc = ' || new_xml || ' WHERE id = ' || bib_id;
-        r = TRUE;
-    END IF;
-
-    RETURN r;
-
-END;
-$BODY$ LANGUAGE plpgsql;
-
--- strip marc tag
-DROP FUNCTION IF EXISTS migration_tools.strip_tag(TEXT,TEXT);
-CREATE OR REPLACE FUNCTION migration_tools.strip_tag(marc TEXT, tag TEXT)
- RETURNS TEXT
- LANGUAGE plperlu
-AS $function$
-use strict;
-use warnings;
-
-use MARC::Record;
-use MARC::File::XML (BinaryEncoding => 'utf8');
-
-binmode(STDERR, ':bytes');
-binmode(STDOUT, ':utf8');
-binmode(STDERR, ':utf8');
-
-my $marc_xml = shift;
-my $tag = shift;
-
-$marc_xml =~ s/(<leader>.........)./${1}a/;
-
-eval {
-    $marc_xml = MARC::Record->new_from_xml($marc_xml);
-};
-if ($@) {
-    #elog("could not parse $bibid: $@\n");
-    import MARC::File::XML (BinaryEncoding => 'utf8');
-    return $marc_xml;
-}
-
-my @fields = $marc_xml->field($tag);
-return $marc_xml->as_xml_record() unless @fields;
-
-$marc_xml->delete_fields(@fields);
-
-return $marc_xml->as_xml_record();
-
-$function$;
+function$;
 
 -- convenience function for linking to the item staging table
 
@@ -5174,3 +5096,356 @@ BEGIN
     RETURN TRUE;
 END
 $function$;
+
+
+-- convenience function for handling item barcode collisions in asset_copy_legacy
+
+CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
+DECLARE
+    x_barcode TEXT;
+    x_id BIGINT;
+    row_count NUMERIC;
+    internal_collision_count NUMERIC := 0;
+    incumbent_collision_count NUMERIC := 0;
+BEGIN
+    FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
+    LOOP
+        FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
+        LOOP
+            UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            internal_collision_count := internal_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% internal collisions', internal_collision_count;
+    FOR x_barcode IN SELECT a.barcode FROM asset.copy a, asset_copy_legacy b WHERE x_migrate AND a.deleted IS FALSE AND a.barcode = b.barcode
+    LOOP
+        FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
+        LOOP
+            UPDATE asset_copy_legacy SET barcode = migration_schema || '_incumbent_collision_' || id || '_' || barcode WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            incumbent_collision_count := incumbent_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% incumbent collisions', incumbent_collision_count;
+END
+$function$ LANGUAGE plpgsql;
+
+-- convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
+-- this should be ran prior to populating actor_card
+
+CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions(migration_schema TEXT) RETURNS VOID AS $function$
+DECLARE
+    x_barcode TEXT;
+    x_id BIGINT;
+    row_count NUMERIC;
+    internal_collision_count NUMERIC := 0;
+    incumbent_barcode_collision_count NUMERIC := 0;
+    incumbent_usrname_collision_count NUMERIC := 0;
+BEGIN
+    FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
+    LOOP
+        FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
+        LOOP
+            UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            internal_collision_count := internal_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
+
+    FOR x_barcode IN
+        SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
+    LOOP
+        FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
+        LOOP
+            UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_barcode_collision_' || id || '_' || usrname WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
+
+    FOR x_barcode IN
+        SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
+    LOOP
+        FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
+        LOOP
+            UPDATE actor_usr_legacy SET usrname = migration_schema || '_incumbent_usrname_collision_' || id || '_' || usrname WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
+END
+$function$ LANGUAGE plpgsql;
+
+-- alternate version: convenience function for handling item barcode collisions in asset_copy_legacy
+
+CREATE OR REPLACE FUNCTION migration_tools.handle_asset_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
+DECLARE
+    x_barcode TEXT;
+    x_id BIGINT;
+    row_count NUMERIC;
+    internal_collision_count NUMERIC := 0;
+    incumbent_collision_count NUMERIC := 0;
+BEGIN
+    FOR x_barcode IN SELECT barcode FROM asset_copy_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
+    LOOP
+        FOR x_id IN SELECT id FROM asset_copy WHERE barcode = x_barcode
+        LOOP
+            UPDATE asset_copy SET barcode = migration_schema || '_internal_collision_' || id || '_' || barcode WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            internal_collision_count := internal_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% internal collisions', internal_collision_count;
+    FOR x_barcode IN SELECT a.barcode FROM asset.copy a, asset_copy_legacy b WHERE x_migrate AND a.deleted IS FALSE AND a.barcode = b.barcode
+    LOOP
+        FOR x_id IN SELECT id FROM asset_copy_legacy WHERE barcode = x_barcode
+        LOOP
+            UPDATE asset_copy_legacy SET barcode = migration_schema || '_' || barcode WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            incumbent_collision_count := incumbent_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% incumbent collisions', incumbent_collision_count;
+END
+$function$ LANGUAGE plpgsql;
+
+-- alternate version: convenience function for handling patron barcode/usrname collisions in actor_usr_legacy
+-- this should be ran prior to populating actor_card
+
+CREATE OR REPLACE FUNCTION migration_tools.handle_actor_barcode_collisions2(migration_schema TEXT) RETURNS VOID AS $function$
+DECLARE
+    x_barcode TEXT;
+    x_id BIGINT;
+    row_count NUMERIC;
+    internal_collision_count NUMERIC := 0;
+    incumbent_barcode_collision_count NUMERIC := 0;
+    incumbent_usrname_collision_count NUMERIC := 0;
+BEGIN
+    FOR x_barcode IN SELECT usrname FROM actor_usr_legacy WHERE x_migrate GROUP BY 1 HAVING COUNT(*) > 1
+    LOOP
+        FOR x_id IN SELECT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
+        LOOP
+            UPDATE actor_usr_legacy SET usrname = migration_schema || '_internal_collision_' || id || '_' || usrname WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            internal_collision_count := internal_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% internal usrname/barcode collisions', internal_collision_count;
+
+    FOR x_barcode IN
+        SELECT a.barcode FROM actor.card a, actor_usr_legacy b WHERE x_migrate AND a.barcode = b.usrname
+    LOOP
+        FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
+        LOOP
+            UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            incumbent_barcode_collision_count := incumbent_barcode_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% incumbent barcode collisions', incumbent_barcode_collision_count;
+
+    FOR x_barcode IN
+        SELECT a.usrname FROM actor.usr a, actor_usr_legacy b WHERE x_migrate AND a.usrname = b.usrname
+    LOOP
+        FOR x_id IN SELECT DISTINCT id FROM actor_usr_legacy WHERE x_migrate AND usrname = x_barcode
+        LOOP
+            UPDATE actor_usr_legacy SET usrname = migration_schema || '_' || usrname WHERE id = x_id;
+            GET DIAGNOSTICS row_count = ROW_COUNT;
+            incumbent_usrname_collision_count := incumbent_usrname_collision_count + row_count;
+        END LOOP;
+    END LOOP;
+    RAISE INFO '% incumbent usrname collisions (post barcode collision munging)', incumbent_usrname_collision_count;
+END
+$function$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION migration_tools.is_circ_rule_safe_to_delete( test_matchpoint INTEGER ) RETURNS BOOLEAN AS $func$
+-- WARNING: Use at your own risk
+-- FIXME: not considering marc_type, marc_form, marc_bib_level, marc_vr_format, usr_age_lower_bound, usr_age_upper_bound, item_age
+DECLARE
+    item_object asset.copy%ROWTYPE;
+    user_object actor.usr%ROWTYPE;
+    test_rule_object config.circ_matrix_matchpoint%ROWTYPE;
+    result_rule_object config.circ_matrix_matchpoint%ROWTYPE;
+    safe_to_delete BOOLEAN := FALSE;
+    m action.found_circ_matrix_matchpoint;
+    n action.found_circ_matrix_matchpoint;
+    -- ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] )
+    result_matchpoint INTEGER;
+BEGIN
+    SELECT INTO test_rule_object * FROM config.circ_matrix_matchpoint WHERE id = test_matchpoint;
+    RAISE INFO 'testing rule: %', test_rule_object;
+
+    INSERT INTO actor.usr (
+        profile,
+        usrname,
+        passwd,
+        ident_type,
+        first_given_name,
+        family_name,
+        home_ou,
+        juvenile
+    ) SELECT
+        COALESCE(test_rule_object.grp, 2),
+        'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
+        MD5(NOW()::TEXT),
+        1,
+        'Ima',
+        'Test',
+        COALESCE(test_rule_object.user_home_ou, test_rule_object.org_unit),
+        COALESCE(test_rule_object.juvenile_flag, FALSE)
+    ;
+    
+    SELECT INTO user_object * FROM actor.usr WHERE id = currval('actor.usr_id_seq');
+
+    INSERT INTO asset.call_number (
+        creator,
+        editor,
+        record,
+        owning_lib,
+        label,
+        label_class
+    ) SELECT
+        1,
+        1,
+        -1,
+        COALESCE(test_rule_object.copy_owning_lib,test_rule_object.org_unit),
+        'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
+        1
+    ;
+
+    INSERT INTO asset.copy (
+        barcode,
+        circ_lib,
+        creator,
+        call_number,
+        editor,
+        location,
+        loan_duration,
+        fine_level,
+        ref,
+        circ_modifier
+    ) SELECT
+        'is_circ_rule_safe_to_delete_' || test_matchpoint || '_' || NOW()::text,
+        COALESCE(test_rule_object.copy_circ_lib,test_rule_object.org_unit),
+        1,
+        currval('asset.call_number_id_seq'),
+        1,
+        COALESCE(test_rule_object.copy_location,1),
+        2,
+        2,
+        COALESCE(test_rule_object.ref_flag,FALSE),
+        test_rule_object.circ_modifier
+    ;
+
+    SELECT INTO item_object * FROM asset.copy WHERE id = currval('asset.copy_id_seq');
+
+    SELECT INTO m * FROM action.find_circ_matrix_matchpoint(
+        test_rule_object.org_unit,
+        item_object,
+        user_object,
+        COALESCE(test_rule_object.is_renewal,FALSE)
+    );
+    RAISE INFO '   action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
+        test_rule_object.org_unit,
+        item_object.id,
+        user_object.id,
+        COALESCE(test_rule_object.is_renewal,FALSE),
+        m.success,
+        m.matchpoint,
+        m.buildrows
+    ;
+
+    --  disable the rule being tested to see if the outcome changes
+    UPDATE config.circ_matrix_matchpoint SET active = FALSE WHERE id = (m.matchpoint).id;
+
+    SELECT INTO n * FROM action.find_circ_matrix_matchpoint(
+        test_rule_object.org_unit,
+        item_object,
+        user_object,
+        COALESCE(test_rule_object.is_renewal,FALSE)
+    );
+    RAISE INFO 'VS action.find_circ_matrix_matchpoint(%,%,%,%) = (%,%,%)',
+        test_rule_object.org_unit,
+        item_object.id,
+        user_object.id,
+        COALESCE(test_rule_object.is_renewal,FALSE),
+        n.success,
+        n.matchpoint,
+        n.buildrows
+    ;
+
+    -- FIXME: We could dig deeper and see if the referenced config.rule_*
+    -- entries are effectively equivalent, but for now, let's assume no
+    -- duplicate rules at that level
+    IF (
+            (m.matchpoint).circulate = (n.matchpoint).circulate
+        AND (m.matchpoint).duration_rule = (n.matchpoint).duration_rule
+        AND (m.matchpoint).recurring_fine_rule = (n.matchpoint).recurring_fine_rule
+        AND (m.matchpoint).max_fine_rule = (n.matchpoint).max_fine_rule
+        AND (
+                (m.matchpoint).hard_due_date = (n.matchpoint).hard_due_date
+                OR (
+                        (m.matchpoint).hard_due_date IS NULL
+                    AND (n.matchpoint).hard_due_date IS NULL
+                )
+        )
+        AND (
+                (m.matchpoint).renewals = (n.matchpoint).renewals
+                OR (
+                        (m.matchpoint).renewals IS NULL
+                    AND (n.matchpoint).renewals IS NULL
+                )
+        )
+        AND (
+                (m.matchpoint).grace_period = (n.matchpoint).grace_period
+                OR (
+                        (m.matchpoint).grace_period IS NULL
+                    AND (n.matchpoint).grace_period IS NULL
+                )
+        )
+        AND (
+                (m.matchpoint).total_copy_hold_ratio = (n.matchpoint).total_copy_hold_ratio
+                OR (
+                        (m.matchpoint).total_copy_hold_ratio IS NULL
+                    AND (n.matchpoint).total_copy_hold_ratio IS NULL
+                )
+        )
+        AND (
+                (m.matchpoint).available_copy_hold_ratio = (n.matchpoint).available_copy_hold_ratio
+                OR (
+                        (m.matchpoint).available_copy_hold_ratio IS NULL
+                    AND (n.matchpoint).available_copy_hold_ratio IS NULL
+                )
+        )
+        AND NOT EXISTS (
+            SELECT limit_set, fallthrough
+            FROM config.circ_matrix_limit_set_map
+            WHERE active and matchpoint = (m.matchpoint).id
+            EXCEPT
+            SELECT limit_set, fallthrough
+            FROM config.circ_matrix_limit_set_map
+            WHERE active and matchpoint = (n.matchpoint).id
+        )
+
+    ) THEN
+        RAISE INFO 'rule has same outcome';
+        safe_to_delete := TRUE;
+    ELSE
+        RAISE INFO 'rule has different outcome';
+        safe_to_delete := FALSE;
+    END IF;
+
+    RAISE EXCEPTION 'rollback the temporary changes';
+
+EXCEPTION WHEN OTHERS THEN
+
+    RAISE INFO 'inside exception block: %, %', SQLSTATE, SQLERRM;
+    RETURN safe_to_delete;
+
+END;
+$func$ LANGUAGE plpgsql;
+