new utility to overlay bib records in an Evergreen database
[migration-tools.git] / eg_staged_bib_overlay
1 #!/usr/bin/perl
2
3 # Copyright (c) 2016 Equinox Software, Inc.
4 # Author: Galen Charlton <gmc@esilibrary.com>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2, or (at your option)
9 # any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>
18
19 use strict;
20 use warnings;
21
22 use Getopt::Long;
23 use MARC::Record;
24 use MARC::File::XML (BinaryEncoding => 'utf8');
25 use DBI;
26 use OpenILS::Application::AppUtils;
27
28 my $action;
29 my $schema = 'bib_loads';
30 my $db;
31 my $dbuser;
32 my $dbpw;
33 my $dbhost;
34 my $batch;
35 my $cutoff;
36 my $wait = 1;
37
38 my $ret = GetOptions(
39     'action:s'      => \$action,
40     'schema:s'      => \$schema,
41     'db:s'          => \$db,
42     'dbuser:s'      => \$dbuser,
43     'dbhost:s'      => \$dbhost,
44     'dbpw:s'        => \$dbpw,
45     'batch:s'       => \$batch,
46     'cutoff:s'      => \$cutoff,
47     'wait:i'        => \$wait,
48 );
49
50 abort('must specify --action') unless defined $action;
51 abort('must specify --schema') unless defined $schema;
52 abort('must specify --db') unless defined $db;
53 abort('must specify --dbuser') unless defined $dbuser;
54 abort('must specify --dbhost') unless defined $dbhost;
55 abort('must specify --dbpw') unless defined $dbpw;
56 abort('must specify --batch') unless defined $batch;
57
58 abort('--action must be "stage_bibs" or "filter_bibs" or "load_bibs"') unless
59     $action eq 'filter_bibs' or
60     $action eq 'stage_bibs' or
61     $action eq 'load_bibs';
62
63 my $dbh = connect_db($db, $dbuser, $dbpw, $dbhost);
64
65 if ($action eq 'stage_bibs') {
66     abort('must specify at least one input file') unless @ARGV;
67     handle_stage_bibs($dbh, $schema, $batch);
68 }
69
70 if ($action eq 'filter_bibs') {
71     abort('must specify cutoff date when filtering') unless defined $cutoff;
72     handle_filter_bibs($dbh, $schema, $batch, $cutoff);
73 }
74
75 if ($action eq 'load_bibs' ) {
76     handle_load_bibs($dbh, $schema, $batch, $wait);
77 }
78
79 sub abort {
80     my $msg = shift;
81     print STDERR "$0: $msg", "\n";
82     print_usage();
83     exit 1;
84 }
85
86 sub print_usage {
87     print <<_USAGE_;
88
89 Utility to stage and overlay bib records in an Evergreen database. This
90 expects that the incoming records will have been previously exported
91 from that Evergreen database and modified in some fashion (e.g., for
92 authority record processing) and that the bib ID can be found in the
93 901\$c subfield.
94
95 This program has several modes controlled by the --action switch:
96
97   --action stage_bibs  - load MARC bib records into a staging table
98   --action filter_bibs - mark previously staged bibs that should
99                          be excluded from a subsequent load, either
100                          because the target bib is deleted in Evergreen
101                          or the record was modified after a date
102                          specified by the --cutoff switch
103   --action load_bibs   - overlay bib records using a previously staged
104                          batch, one at a time. After each bib, it will
105                          wait the number of seconds specified by the
106                          --wait switch.
107
108 Several switches are used regardless of the specified action:
109
110   --schema  - Pg schema in which staging table will live; should be
111               created beforehand
112   --batch   - name of bib batch; will also be used as the name
113               of the staging table
114   --db      - database name
115   --dbuser  - database user
116   --dbpw    - database password
117   --dbhost  - database host
118
119 Examples:
120
121 $0 --schema bib_load --batch bibs_2016_01 --db evergreen \\
122    --dbuser evergreen --dbpw evergreen --dbhost localhost \\
123    --action stage_bibs -- file1.mrc file2.mrc [...]
124
125 $0 --schema bib_load --batch bibs_2016_01 --db evergreen \\
126    --dbuser evergreen --dbpw evergreen --dbhost localhost \\
127    --action filter_bibs --cutoff 2016-01-02
128
129 $0 --schema bib_load --batch bibs_2016_01 --db evergreen \\
130    --dbuser evergreen --dbpw evergreen --dbhost localhost \\
131    --action load_bibs
132
133 _USAGE_
134 }
135
136
137 sub report_progress {
138     my ($msg, $counter) = @_;
139     if (defined $counter) {
140         print STDERR "$msg: $counter\n";
141     } else {
142         print STDERR "$msg\n";
143     }
144 }
145
146 sub connect_db {
147     my ($db, $dbuser, $dbpw, $dbhost) = @_;
148
149     my $dsn = "dbi:Pg:host=$dbhost;dbname=$db;port=5432";
150
151     my $attrs = {
152         ShowErrorStatement => 1,
153         RaiseError => 1,
154         PrintError => 1,
155         pg_enable_utf8 => 1,
156     };
157     my $dbh = DBI->connect($dsn, $dbuser, $dbpw, $attrs);
158
159     return $dbh;
160 }
161
162 sub handle_stage_bibs {
163     my $dbh = shift;
164     my $schema = shift;
165     my $batch = shift;
166
167     $dbh->do(qq{
168         DROP TABLE IF EXISTS $schema.$batch;
169     });
170     $dbh->do(qq{
171         CREATE TABLE $schema.$batch (
172             id          SERIAL,
173             marc        TEXT,
174             bib_id      BIGINT,
175             imported    BOOLEAN DEFAULT FALSE,
176             to_import   BOOLEAN DEFAULT TRUE,
177             skip_reason TEXT
178         )
179     });
180
181     local $/ = "\035";
182     my $i = 0;
183     binmode STDIN, ':utf8';
184     my $ins = $dbh->prepare("INSERT INTO $schema.$batch (marc, bib_id) VALUES (?, ?)");
185     $dbh->begin_work;
186     while (<>) {
187         $i++;
188         if (0 == $i % 100) {
189             report_progress("Records staged", $i);
190             $dbh->commit;
191             $dbh->begin_work;
192         }
193         my $marc = MARC::Record->new_from_usmarc($_);
194         my $bibid = $marc->subfield('901', 'c');
195         if ($bibid !~ /^\d+$/) {
196             print STDERR "Record $i is suspect; skipping\n";
197             next;
198         }
199         my $xml = OpenILS::Application::AppUtils->entityize($marc->as_xml_record());
200         $ins->execute($xml, $bibid);
201     }
202     $dbh->commit;
203     report_progress("Records staged", $i) if 0 != $i % 100;
204     $dbh->do(qq/
205         CREATE INDEX ${batch}_bib_id_idx ON
206             $schema.$batch (bib_id);
207     /);
208     $dbh->do(qq/
209         CREATE INDEX ${batch}_id_idx ON
210             $schema.$batch (id);
211     /);
212 }
213
214 sub handle_filter_bibs {
215     my $dbh = shift;
216     my $schema = shift;
217     my $batch = shift;
218     my $cutoff = shift;
219
220     my $sth1 = $dbh->prepare(qq{
221         UPDATE $schema.$batch
222         SET to_import = FALSE,
223             skip_reason = 'deleted'
224         WHERE bib_id IN (
225             SELECT id
226             FROM biblio.record_entry
227             WHERE deleted
228         )
229     });
230     $sth1->execute();
231     my $ct = $sth1->rows;
232     report_progress("Filtering out $ct records that are currently deleted");
233
234     my $sth2 = $dbh->prepare(qq{
235         UPDATE $schema.$batch
236         SET to_import = FALSE,
237             skip_reason = 'edited after cutoff of $cutoff'
238         WHERE bib_id IN (
239             SELECT id
240             FROM biblio.record_entry
241             WHERE edit_date >= ?
242         )
243         AND to_import;
244     });
245     $sth2->execute($cutoff);
246     $ct = $sth2->rows;
247     report_progress("Filtering out $ct records edited after cutoff date of $cutoff");
248 }
249
250 sub handle_load_bibs {
251     my $dbh = shift;
252     my $schema = shift;
253     my $batch = shift;
254     my $wait = shift;
255
256     my $getct = $dbh->prepare(qq{
257         SELECT COUNT(*)
258         FROM  $schema.$batch
259         WHERE to_import
260         AND NOT imported
261     });
262     $getct->execute();
263     my $max = $getct->fetchrow_arrayref()->[0];
264
265     report_progress('Number of bibs to update', $max);
266     for (my $i = 1; $i <= $max; $i++) {
267         report_progress('... bibs updated', $i) if 0 == $i % 10 or $i == $max;
268         $dbh->begin_work;
269         $dbh->do(qq{
270             UPDATE biblio.record_entry a
271             SET marc = b.marc
272             FROM $schema.$batch b
273             WHERE a.id = b.bib_id
274             AND bib_id IN (
275                 SELECT bib_id
276                 FROM $schema.$batch
277                 WHERE to_import
278                 AND NOT imported
279                 ORDER BY id
280                 LIMIT 1
281             )
282         });
283         $dbh->do(qq{
284             UPDATE $schema.$batch
285             SET imported = TRUE
286             WHERE bib_id IN (
287                 SELECT bib_id
288                 FROM $schema.$batch
289                 WHERE to_import
290                 AND NOT imported
291                 ORDER BY id
292                 LIMIT 1
293             )
294         });
295         $dbh->commit;
296         sleep $wait;
297     }
298     
299 }