76bcf62c96ce1183398d567bfa0ece2d3424f14c
[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 --wait 2
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         AND to_import
230         AND NOT imported
231     });
232     $sth1->execute();
233     my $ct = $sth1->rows;
234     report_progress("Filtering out $ct records that are currently deleted");
235
236     my $sth2 = $dbh->prepare(qq{
237         UPDATE $schema.$batch
238         SET to_import = FALSE,
239             skip_reason = 'edited after cutoff of $cutoff'
240         WHERE bib_id IN (
241             SELECT id
242             FROM biblio.record_entry
243             WHERE edit_date >= ?
244         )
245         AND to_import
246         AND NOT imported
247     });
248     $sth2->execute($cutoff);
249     $ct = $sth2->rows;
250     report_progress("Filtering out $ct records edited after cutoff date of $cutoff");
251
252     my $sth3 = $dbh->prepare(qq{
253         UPDATE $schema.$batch
254         SET to_import = FALSE,
255             skip_reason = 'XML is not well-formed'
256         WHERE NOT xml_is_well_formed(marc)
257         AND to_import
258         AND NOT imported
259     });
260     $sth3->execute();
261     $ct = $sth3->rows;
262     report_progress("Filtering out $ct records whose XML is not well-formed");
263 }
264
265 sub handle_load_bibs {
266     my $dbh = shift;
267     my $schema = shift;
268     my $batch = shift;
269     my $wait = shift;
270
271     my $getct = $dbh->prepare(qq{
272         SELECT COUNT(*)
273         FROM  $schema.$batch
274         WHERE to_import
275         AND NOT imported
276     });
277     $getct->execute();
278     my $max = $getct->fetchrow_arrayref()->[0];
279
280     report_progress('Number of bibs to update', $max);
281     for (my $i = 1; $i <= $max; $i++) {
282         report_progress('... bibs updated', $i) if 0 == $i % 10 or $i == $max;
283         $dbh->begin_work;
284         $dbh->do(qq{
285             UPDATE biblio.record_entry a
286             SET marc = b.marc
287             FROM $schema.$batch b
288             WHERE a.id = b.bib_id
289             AND bib_id IN (
290                 SELECT bib_id
291                 FROM $schema.$batch
292                 WHERE to_import
293                 AND NOT imported
294                 ORDER BY id
295                 LIMIT 1
296             )
297         });
298         $dbh->do(qq{
299             UPDATE $schema.$batch
300             SET imported = TRUE
301             WHERE bib_id IN (
302                 SELECT bib_id
303                 FROM $schema.$batch
304                 WHERE to_import
305                 AND NOT imported
306                 ORDER BY id
307                 LIMIT 1
308             )
309         });
310         $dbh->commit;
311         sleep $wait;
312     }
313     
314 }