]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/030.schema.metabib.sql
provide indexes for the new faceting code
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 030.schema.metabib.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2007-2008  Equinox Software, Inc.
4  * Mike Rylander <miker@esilibrary.com> 
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) 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  */
17
18 DROP SCHEMA metabib CASCADE;
19
20 BEGIN;
21 CREATE SCHEMA metabib;
22
23 CREATE TABLE metabib.metarecord (
24         id              BIGSERIAL       PRIMARY KEY,
25         fingerprint     TEXT            NOT NULL,
26         master_record   BIGINT,
27         mods            TEXT
28 );
29 CREATE INDEX metabib_metarecord_master_record_idx ON metabib.metarecord (master_record);
30 CREATE INDEX metabib_metarecord_fingerprint_idx ON metabib.metarecord (fingerprint);
31
32 CREATE TABLE metabib.title_field_entry (
33         id              BIGSERIAL       PRIMARY KEY,
34         source          BIGINT          NOT NULL,
35         field           INT             NOT NULL,
36         value           TEXT            NOT NULL,
37         index_vector    tsvector        NOT NULL
38 );
39 CREATE TRIGGER metabib_title_field_entry_fti_trigger
40         BEFORE UPDATE OR INSERT ON metabib.title_field_entry
41         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('title');
42
43 CREATE INDEX metabib_title_field_entry_index_vector_idx ON metabib.title_field_entry USING GIST (index_vector);
44 CREATE INDEX metabib_title_field_entry_value_idx ON metabib.title_field_entry (SUBSTRING(value,1,1024));
45 CREATE INDEX metabib_title_field_entry_source_idx ON metabib.title_field_entry (source);
46
47
48 CREATE TABLE metabib.author_field_entry (
49         id              BIGSERIAL       PRIMARY KEY,
50         source          BIGINT          NOT NULL,
51         field           INT             NOT NULL,
52         value           TEXT            NOT NULL,
53         index_vector    tsvector        NOT NULL
54 );
55 CREATE TRIGGER metabib_author_field_entry_fti_trigger
56         BEFORE UPDATE OR INSERT ON metabib.author_field_entry
57         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('author');
58
59 CREATE INDEX metabib_author_field_entry_index_vector_idx ON metabib.author_field_entry USING GIST (index_vector);
60 CREATE INDEX metabib_author_field_entry_value_idx ON metabib.author_field_entry (SUBSTRING(value,1,1024));
61 CREATE INDEX metabib_author_field_entry_source_idx ON metabib.author_field_entry (source);
62
63
64 CREATE TABLE metabib.subject_field_entry (
65         id              BIGSERIAL       PRIMARY KEY,
66         source          BIGINT          NOT NULL,
67         field           INT             NOT NULL,
68         value           TEXT            NOT NULL,
69         index_vector    tsvector        NOT NULL
70 );
71 CREATE TRIGGER metabib_subject_field_entry_fti_trigger
72         BEFORE UPDATE OR INSERT ON metabib.subject_field_entry
73         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('subject');
74
75 CREATE INDEX metabib_subject_field_entry_index_vector_idx ON metabib.subject_field_entry USING GIST (index_vector);
76 CREATE INDEX metabib_subject_field_entry_value_idx ON metabib.subject_field_entry (SUBSTRING(value,1,1024));
77 CREATE INDEX metabib_subject_field_entry_source_idx ON metabib.subject_field_entry (source);
78
79
80 CREATE TABLE metabib.keyword_field_entry (
81         id              BIGSERIAL       PRIMARY KEY,
82         source          BIGINT          NOT NULL,
83         field           INT             NOT NULL,
84         value           TEXT            NOT NULL,
85         index_vector    tsvector        NOT NULL
86 );
87 CREATE TRIGGER metabib_keyword_field_entry_fti_trigger
88         BEFORE UPDATE OR INSERT ON metabib.keyword_field_entry
89         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
90
91 CREATE INDEX metabib_keyword_field_entry_index_vector_idx ON metabib.keyword_field_entry USING GIST (index_vector);
92 CREATE INDEX metabib_keyword_field_entry_value_idx ON metabib.keyword_field_entry (SUBSTRING(value,1,1024));
93 CREATE INDEX metabib_keyword_field_entry_source_idx ON metabib.keyword_field_entry (source);
94
95
96 CREATE TABLE metabib.series_field_entry (
97         id              BIGSERIAL       PRIMARY KEY,
98         source          BIGINT          NOT NULL,
99         field           INT             NOT NULL,
100         value           TEXT            NOT NULL,
101         index_vector    tsvector        NOT NULL
102 );
103 CREATE TRIGGER metabib_series_field_entry_fti_trigger
104         BEFORE UPDATE OR INSERT ON metabib.series_field_entry
105         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('series');
106
107 CREATE INDEX metabib_series_field_entry_index_vector_idx ON metabib.series_field_entry USING GIST (index_vector);
108 CREATE INDEX metabib_series_field_entry_value_idx ON metabib.series_field_entry (SUBSTRING(value,1,1024));
109 CREATE INDEX metabib_series_field_entry_source_idx ON metabib.series_field_entry (source);
110
111
112 CREATE TABLE metabib.rec_descriptor (
113         id              BIGSERIAL PRIMARY KEY,
114         record          BIGINT,
115         item_type       TEXT,
116         item_form       TEXT,
117         bib_level       TEXT,
118         control_type    TEXT,
119         char_encoding   TEXT,
120         enc_level       TEXT,
121         audience        TEXT,
122         lit_form        TEXT,
123         type_mat        TEXT,
124         cat_form        TEXT,
125         pub_status      TEXT,
126         item_lang       TEXT,
127         vr_format       TEXT,
128         date1           TEXT,
129         date2           TEXT
130 );
131 CREATE INDEX metabib_rec_descriptor_record_idx ON metabib.rec_descriptor (record);
132 /* We may not need these...
133
134 CREATE INDEX metabib_rec_descriptor_item_type_idx ON metabib.rec_descriptor (item_type);
135 CREATE INDEX metabib_rec_descriptor_item_form_idx ON metabib.rec_descriptor (item_form);
136 CREATE INDEX metabib_rec_descriptor_bib_level_idx ON metabib.rec_descriptor (bib_level);
137 CREATE INDEX metabib_rec_descriptor_control_type_idx ON metabib.rec_descriptor (control_type);
138 CREATE INDEX metabib_rec_descriptor_char_encoding_idx ON metabib.rec_descriptor (char_encoding);
139 CREATE INDEX metabib_rec_descriptor_enc_level_idx ON metabib.rec_descriptor (enc_level);
140 CREATE INDEX metabib_rec_descriptor_audience_idx ON metabib.rec_descriptor (audience);
141 CREATE INDEX metabib_rec_descriptor_lit_form_idx ON metabib.rec_descriptor (lit_form);
142 CREATE INDEX metabib_rec_descriptor_cat_form_idx ON metabib.rec_descriptor (cat_form);
143 CREATE INDEX metabib_rec_descriptor_pub_status_idx ON metabib.rec_descriptor (pub_status);
144 CREATE INDEX metabib_rec_descriptor_item_lang_idx ON metabib.rec_descriptor (item_lang);
145 CREATE INDEX metabib_rec_descriptor_vr_format_idx ON metabib.rec_descriptor (vr_format);
146
147 */
148
149 -- Use a sequence that matches previous version, for easier upgrading.
150 CREATE SEQUENCE metabib.full_rec_id_seq;
151
152 CREATE TABLE metabib.real_full_rec (
153         id                  BIGINT      NOT NULL DEFAULT NEXTVAL('metabib.full_rec_id_seq'::REGCLASS),
154         record          BIGINT          NOT NULL,
155         tag             CHAR(3)         NOT NULL,
156         ind1            TEXT,
157         ind2            TEXT,
158         subfield        TEXT,
159         value           TEXT            NOT NULL,
160         index_vector    tsvector        NOT NULL
161 );
162 ALTER TABLE metabib.real_full_rec ADD PRIMARY KEY (id);
163
164 CREATE INDEX metabib_full_rec_tag_subfield_idx ON metabib.real_full_rec (tag,subfield);
165 CREATE INDEX metabib_full_rec_value_idx ON metabib.real_full_rec (substring(value,1,1024));
166 /* Enable LIKE to use an index for database clusters with locales other than C or POSIX */
167 CREATE INDEX metabib_full_rec_value_tpo_index ON metabib.real_full_rec (substring(value,1,1024) text_pattern_ops);
168 CREATE INDEX metabib_full_rec_record_idx ON metabib.real_full_rec (record);
169 CREATE INDEX metabib_full_rec_index_vector_idx ON metabib.real_full_rec USING GIST (index_vector);
170
171 CREATE TRIGGER metabib_full_rec_fti_trigger
172         BEFORE UPDATE OR INSERT ON metabib.real_full_rec
173         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('default');
174
175 CREATE OR REPLACE VIEW metabib.full_rec AS
176     SELECT  id,
177             record,
178             tag,
179             ind1,
180             ind2,
181             subfield,
182             SUBSTRING(value,1,1024) AS value,
183             index_vector
184       FROM  metabib.real_full_rec;
185
186 CREATE OR REPLACE RULE metabib_full_rec_insert_rule
187     AS ON INSERT TO metabib.full_rec
188     DO INSTEAD
189     INSERT INTO metabib.real_full_rec VALUES (
190         COALESCE(NEW.id, NEXTVAL('metabib.full_rec_id_seq'::REGCLASS)),
191         NEW.record,
192         NEW.tag,
193         NEW.ind1,
194         NEW.ind2,
195         NEW.subfield,
196         NEW.value,
197         NEW.index_vector
198     );
199
200 CREATE OR REPLACE RULE metabib_full_rec_update_rule
201     AS ON UPDATE TO metabib.full_rec
202     DO INSTEAD
203     UPDATE  metabib.real_full_rec SET
204         id = NEW.id,
205         record = NEW.record,
206         tag = NEW.tag,
207         ind1 = NEW.ind1,
208         ind2 = NEW.ind2,
209         subfield = NEW.subfield,
210         value = NEW.value,
211         index_vector = NEW.index_vector
212       WHERE id = OLD.id;
213
214 CREATE OR REPLACE RULE metabib_full_rec_delete_rule
215     AS ON DELETE TO metabib.full_rec
216     DO INSTEAD
217     DELETE FROM metabib.real_full_rec WHERE id = OLD.id;
218
219 CREATE TABLE metabib.metarecord_source_map (
220         id              BIGSERIAL       PRIMARY KEY,
221         metarecord      BIGINT          NOT NULL,
222         source          BIGINT          NOT NULL
223 );
224 CREATE INDEX metabib_metarecord_source_map_metarecord_idx ON metabib.metarecord_source_map (metarecord);
225 CREATE INDEX metabib_metarecord_source_map_source_record_idx ON metabib.metarecord_source_map (source);
226
227 CREATE TYPE metabib.field_entry_template AS (
228         field_class     TEXT,
229         field           INT,
230         source          BIGINT,
231         value           TEXT
232 );
233
234 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( rid BIGINT, default_joiner TEXT ) RETURNS SETOF metabib.field_entry_template AS $func$
235 DECLARE
236         bib             biblio.record_entry%ROWTYPE;
237         idx             config.metabib_field%ROWTYPE;
238         xfrm            config.xml_transform%ROWTYPE;
239         prev_xfrm       TEXT;
240         transformed_xml TEXT;
241         xml_node        TEXT;
242         xml_node_list   TEXT[];
243         raw_text        TEXT;
244         joiner          TEXT := default_joiner; -- XXX will index defs supply a joiner?
245         output_row      metabib.field_entry_template%ROWTYPE;
246 BEGIN
247
248         -- Get the record
249         SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
250
251         -- Loop over the indexing entries
252         FOR idx IN SELECT * FROM config.metabib_field ORDER BY format LOOP
253
254                 SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
255
256                 -- See if we can skip the XSLT ... it's expensive
257                 IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
258                         -- Can't skip the transform
259                         IF xfrm.xslt <> '---' THEN
260                                 transformed_xml := oils_xslt_process(bib.marc,xfrm.xslt);
261                         ELSE
262                                 transformed_xml := bib.marc;
263                         END IF;
264
265                         prev_xfrm := xfrm.name;
266                 END IF;
267
268                 xml_node_list := oils_xpath( idx.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
269
270                 raw_text := NULL;
271                 FOR xml_node IN SELECT x FROM explode_array(xml_node_list) AS x LOOP
272                         CONTINUE WHEN xml_node !~ E'^\\s*<';
273                         IF raw_text IS NOT NULL THEN
274                                 raw_text := raw_text || joiner;
275                         END IF;
276                         raw_text := COALESCE(raw_text,'') || ARRAY_TO_STRING(
277                                 oils_xpath( '//text()',
278                                         REGEXP_REPLACE( -- This escapes all &s not followed by "amp;".  Data ise returned from oils_xpath (above) in UTF-8, not entity encoded
279                                                 REGEXP_REPLACE( -- This escapes embeded <s
280                                                         xml_node,
281                                                         $re$(>[^<]+)(<)([^>]+<)$re$,
282                                                         E'\\1&lt;\\3',
283                                                         'g'
284                                                 ),
285                                                 '&(?!amp;)',
286                                                 '&amp;',
287                                                 'g'
288                                         )
289                                 ),
290                                 ' '
291                         );
292                 END LOOP;
293
294                 CONTINUE WHEN raw_text IS NULL;
295
296                 output_row.field_class = idx.field_class;
297                 output_row.field = idx.id;
298                 output_row.source = rid;
299                 output_row.value = BTRIM(REGEXP_REPLACE(raw_text, E'\\s+', ' ', 'g'));
300
301                 RETURN NEXT output_row;
302
303         END LOOP;
304
305 END;
306 $func$ LANGUAGE PLPGSQL;
307
308 -- default to a space joiner
309 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( BIGINT ) RETURNS SETOF metabib.field_entry_template AS $func$
310         SELECT * FROM biblio.extract_metabib_field_entry($1, ' ');
311 $func$ LANGUAGE SQL;
312
313 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
314 DECLARE
315         bib     biblio.record_entry%ROWTYPE;
316         output  metabib.full_rec%ROWTYPE;
317         field   RECORD;
318 BEGIN
319         SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
320
321         FOR field IN SELECT * FROM biblio.flatten_marc( bib.marc ) LOOP
322                 output.record := rid;
323                 output.ind1 := field.ind1;
324                 output.ind2 := field.ind2;
325                 output.tag := field.tag;
326                 output.subfield := field.subfield;
327                 IF field.subfield IS NOT NULL AND field.tag NOT IN ('020','022','024') THEN -- exclude standard numbers and control fields
328                         output.value := naco_normalize(field.value, field.subfield);
329                 ELSE
330                         output.value := field.value;
331                 END IF;
332
333                 CONTINUE WHEN output.value IS NULL;
334
335                 RETURN NEXT output;
336         END LOOP;
337 END;
338 $func$ LANGUAGE PLPGSQL;
339
340 /* Old form of biblio.flatten_marc() relied on contrib/xml2 functions that got all crashy in PostgreSQL 8.4 */
341 -- CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT, BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
342 --     SELECT  NULL::bigint AS id, NULL::bigint, 'LDR'::char(3), NULL::TEXT, NULL::TEXT, NULL::TEXT, oils_xpath_string( '//*[local-name()="leader"]', $1 ), NULL::tsvector AS index_vector
343 --         UNION
344 --     SELECT  NULL::bigint AS id, NULL::bigint, x.tag::char(3), NULL::TEXT, NULL::TEXT, NULL::TEXT, x.value, NULL::tsvector AS index_vector
345 --       FROM  oils_xpath_table(
346 --                 'id',
347 --                 'marc',
348 --                 'biblio.record_entry',
349 --                 '//*[local-name()="controlfield"]/@tag|//*[local-name()="controlfield"]',
350 --                 'id=' || $2::TEXT
351 --             )x(record int, tag text, value text)
352 --         UNION
353 --     SELECT  NULL::bigint AS id, NULL::bigint, x.tag::char(3), x.ind1, x.ind2, x.subfield, x.value, NULL::tsvector AS index_vector
354 --       FROM  oils_xpath_table(
355 --                 'id',
356 --                 'marc',
357 --                 'biblio.record_entry',
358 --                 '//*[local-name()="datafield"]/@tag|' ||
359 --                 '//*[local-name()="datafield"]/@ind1|' ||
360 --                 '//*[local-name()="datafield"]/@ind2|' ||
361 --                 '//*[local-name()="datafield"]/*/@code|' ||
362 --                 '//*[local-name()="datafield"]/*[@code]',
363 --                 'id=' || $2::TEXT
364 --             )x(record int, tag text, ind1 text, ind2 text, subfield text, value text);
365 -- $func$ LANGUAGE SQL;
366
367 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
368
369 use MARC::Record;
370 use MARC::File::XML (BinaryEncoding => 'UTF-8');
371
372 my $xml = shift;
373 my $r = MARC::Record->new_from_xml( $xml );
374
375 return_next( { tag => 'LDR', value => $r->leader } );
376
377 for my $f ( $r->fields ) {
378         if ($f->is_control_field) {
379                 return_next({ tag => $f->tag, value => $f->data });
380         } else {
381                 for my $s ($f->subfields) {
382                         return_next({
383                                 tag      => $f->tag,
384                                 ind1     => $f->indicator(1),
385                                 ind2     => $f->indicator(2),
386                                 subfield => $s->[0],
387                                 value    => $s->[1]
388                         });
389
390                         if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
391                                 my $trim = $f->indicator(2) || 0;
392                                 return_next({
393                                         tag      => 'tnf',
394                                         ind1     => $f->indicator(1),
395                                         ind2     => $f->indicator(2),
396                                         subfield => 'a',
397                                         value    => substr( $s->[1], $trim )
398                                 });
399                         }
400                 }
401         }
402 }
403
404 return undef;
405
406 $func$ LANGUAGE PLPERLU;
407
408 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
409 DECLARE
410         ldr         RECORD;
411         tval        TEXT;
412         tval_rec    RECORD;
413         bval        TEXT;
414         bval_rec    RECORD;
415     retval      config.marc21_rec_type_map%ROWTYPE;
416 BEGIN
417     SELECT * INTO ldr FROM metabib.full_rec WHERE record = rid AND tag = 'LDR' LIMIT 1;
418
419     IF ldr.id IS NULL THEN
420         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
421         RETURN retval;
422     END IF;
423
424     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
425     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
426
427
428     tval := SUBSTRING( ldr.value, tval_rec.start_pos + 1, tval_rec.length );
429     bval := SUBSTRING( ldr.value, bval_rec.start_pos + 1, bval_rec.length );
430
431     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr.value;
432
433     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
434
435
436     IF retval.code IS NULL THEN
437         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
438     END IF;
439
440     RETURN retval;
441 END;
442 $func$ LANGUAGE PLPGSQL;
443
444 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
445 DECLARE
446     rtype       TEXT;
447     ff_pos      RECORD;
448     tag_data    RECORD;
449     val         TEXT;
450 BEGIN
451     rtype := (biblio.marc21_record_type( rid )).code;
452     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
453         FOR tag_data IN SELECT * FROM metabib.full_rec WHERE tag = UPPER(ff_pos.tag) AND record = rid LOOP
454             val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
455             RETURN val;
456         END LOOP;
457         val := REPEAT( ff_pos.default_val, ff_pos.length );
458         RETURN val;
459     END LOOP;
460
461     RETURN NULL;
462 END;
463 $func$ LANGUAGE PLPGSQL;
464
465 CREATE TYPE biblio.marc21_physical_characteristics AS ( id INT, record BIGINT, ptype TEXT, subfield INT, value INT );
466 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
467 DECLARE
468     rowid   INT := 0;
469     _007    RECORD;
470     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
471     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
472     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
473     retval  biblio.marc21_physical_characteristics%ROWTYPE;
474 BEGIN
475
476     SELECT * INTO _007 FROM metabib.full_rec WHERE record = rid AND tag = '007' LIMIT 1;
477
478     IF _007.id IS NOT NULL THEN
479         SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007.value, 1, 1 );
480
481         IF ptype.ptype_key IS NOT NULL THEN
482             FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
483                 SELECT * INTO pval FROM config.marc21_physical_characteristic_value_map WHERE ptype_subfield = psf.id AND value = SUBSTRING( _007.value, psf.start_pos + 1, psf.length );
484
485                 IF pval.id IS NOT NULL THEN
486                     rowid := rowid + 1;
487                     retval.id := rowid;
488                     retval.record := rid;
489                     retval.ptype := ptype.ptype_key;
490                     retval.subfield := psf.id;
491                     retval.value := pval.id;
492                     RETURN NEXT retval;
493                 END IF;
494
495             END LOOP;
496         END IF;
497     END IF;
498
499     RETURN;
500 END;
501 $func$ LANGUAGE PLPGSQL;
502
503 CREATE OR REPLACE FUNCTION biblio.extract_quality ( marc TEXT, best_lang TEXT, best_type TEXT ) RETURNS INT AS $func$
504 DECLARE
505     qual        INT;
506     ldr         TEXT;
507     tval        TEXT;
508     tval_rec    RECORD;
509     bval        TEXT;
510     bval_rec    RECORD;
511     type_map    RECORD;
512     ff_pos      RECORD;
513     ff_tag_data TEXT;
514 BEGIN
515
516     IF marc IS NULL OR marc = '' THEN
517         RETURN NULL;
518     END IF;
519
520     -- First, the count of tags
521     qual := ARRAY_UPPER(oils_xpath('*[local-name()="datafield"]', marc), 1);
522
523     -- now go through a bunch of pain to get the record type
524     IF best_type IS NOT NULL THEN
525         ldr := (oils_xpath('//*[local-name()="leader"]/text()', marc))[1];
526
527         IF ldr IS NOT NULL THEN
528             SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
529             SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
530
531
532             tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
533             bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
534
535             -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
536
537             SELECT * INTO type_map FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
538
539             IF type_map.code IS NOT NULL THEN
540                 IF best_type = type_map.code THEN
541                     qual := qual + qual / 2;
542                 END IF;
543
544                 FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = 'Lang' AND rec_type = type_map.code ORDER BY tag DESC LOOP
545                     ff_tag_data := SUBSTRING((oils_xpath('//*[@tag="' || ff_pos.tag || '"]/text()',marc))[1], ff_pos.start_pos + 1, ff_pos.length);
546                     IF ff_tag_data = best_lang THEN
547                             qual := qual + 100;
548                     END IF;
549                 END LOOP;
550             END IF;
551         END IF;
552     END IF;
553
554     -- Now look for some quality metrics
555     -- DCL record?
556     IF ARRAY_UPPER(oils_xpath('//*[@tag="040"]/*[@code="a" and contains(.,"DLC")]', marc), 1) = 1 THEN
557         qual := qual + 10;
558     END IF;
559
560     -- From OCLC?
561     IF (oils_xpath('//*[@tag="003"]/text()', marc))[1] ~* E'oclo?c' THEN
562         qual := qual + 10;
563     END IF;
564
565     RETURN qual;
566
567 END;
568 $func$ LANGUAGE PLPGSQL;
569
570 CREATE OR REPLACE FUNCTION biblio.extract_fingerprint ( marc text ) RETURNS TEXT AS $func$
571 DECLARE
572         idx             config.biblio_fingerprint%ROWTYPE;
573         xfrm            config.xml_transform%ROWTYPE;
574         prev_xfrm       TEXT;
575         transformed_xml TEXT;
576         xml_node        TEXT;
577         xml_node_list   TEXT[];
578         raw_text        TEXT;
579     output_text TEXT := '';
580 BEGIN
581
582     IF marc IS NULL OR marc = '' THEN
583         RETURN NULL;
584     END IF;
585
586         -- Loop over the indexing entries
587         FOR idx IN SELECT * FROM config.biblio_fingerprint ORDER BY format, id LOOP
588
589                 SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
590
591                 -- See if we can skip the XSLT ... it's expensive
592                 IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
593                         -- Can't skip the transform
594                         IF xfrm.xslt <> '---' THEN
595                                 transformed_xml := oils_xslt_process(marc,xfrm.xslt);
596                         ELSE
597                                 transformed_xml := marc;
598                         END IF;
599
600                         prev_xfrm := xfrm.name;
601                 END IF;
602
603                 raw_text := COALESCE(
604             naco_normalize(
605                 ARRAY_TO_STRING(
606                     oils_xpath(
607                         '//text()',
608                         (oils_xpath(
609                             idx.xpath,
610                             transformed_xml,
611                             ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] 
612                         ))[1]
613                     ),
614                     ''
615                 )
616             ),
617             ''
618         );
619
620         raw_text := REGEXP_REPLACE(raw_text, E'\\[.+?\\]', E'');
621         raw_text := REGEXP_REPLACE(raw_text, E'\\mthe\\M|\\man?d?d\\M', E'', 'g'); -- arg! the pain!
622
623         IF idx.first_word IS TRUE THEN
624             raw_text := REGEXP_REPLACE(raw_text, E'^(\\w+).*?$', E'\\1');
625         END IF;
626
627                 output_text := output_text || REGEXP_REPLACE(raw_text, E'\\s+', '', 'g');
628
629         END LOOP;
630
631     RETURN output_text;
632
633 END;
634 $func$ LANGUAGE PLPGSQL;
635
636 -- BEFORE UPDATE OR INSERT trigger for biblio.record_entry
637 CREATE OR REPLACE FUNCTION biblio.fingerprint_trigger () RETURNS TRIGGER AS $func$
638 BEGIN
639
640     -- For TG_ARGV, first param is language (like 'eng'), second is record type (like 'BKS')
641
642     IF NEW.deleted IS TRUE THEN -- we don't much care, then, do we?
643         RETURN NEW;
644     END IF;
645
646     NEW.fingerprint := biblio.extract_fingerprint(NEW.marc);
647     NEW.quality := biblio.extract_quality(NEW.marc, TG_ARGV[0], TG_ARGV[1]);
648
649     RETURN NEW;
650
651 END;
652 $func$ LANGUAGE PLPGSQL;
653
654 -- AFTER UPDATE OR INSERT trigger for biblio.record_entry
655 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
656 DECLARE
657     ind_data        metabib.field_entry_template%ROWTYPE;
658     old_mr          INT;
659     tmp_mr          metabib.metarecord%ROWTYPE;
660     source_count    INT;
661     deleted_mrs     INT[];
662     uris            TEXT[];
663     uri_xml         TEXT;
664     uri_label       TEXT;
665     uri_href        TEXT;
666     uri_use         TEXT;
667     uri_owner       TEXT;
668     uri_owner_id    INT;
669     uri_id          INT;
670     uri_cn_id       INT;
671     uri_map_id      INT;
672 BEGIN
673
674     IF NEW.deleted IS TRUE THEN
675         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
676         RETURN NEW; -- and we're done
677     END IF;
678
679     IF TG_OP = 'UPDATE' THEN -- re-ingest?
680         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
681
682         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
683             RETURN NEW;
684         END IF;
685
686         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
687
688     END IF;
689
690     IF TG_OP = 'UPDATE' THEN -- Clean out the cruft
691         DELETE FROM metabib.title_field_entry WHERE source = NEW.id;
692         DELETE FROM metabib.author_field_entry WHERE source = NEW.id;
693         DELETE FROM metabib.subject_field_entry WHERE source = NEW.id;
694         DELETE FROM metabib.keyword_field_entry WHERE source = NEW.id;
695         DELETE FROM metabib.series_field_entry WHERE source = NEW.id;
696         DELETE FROM metabib.full_rec WHERE record = NEW.id;
697         DELETE FROM metabib.rec_descriptor WHERE record = NEW.id;
698
699     END IF;
700
701     -- Shove the flattened MARC in
702     INSERT INTO metabib.full_rec (record, tag, ind1, ind2, subfield, value)
703         SELECT record, tag, ind1, ind2, subfield, value FROM biblio.flatten_marc( NEW.id );
704
705     -- And now the indexing data
706     FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( NEW.id ) LOOP
707         IF ind_data.field_class = 'title' THEN
708             INSERT INTO metabib.title_field_entry (field, source, value)
709                 VALUES (ind_data.field, ind_data.source, ind_data.value);
710         ELSIF ind_data.field_class = 'author' THEN
711             INSERT INTO metabib.author_field_entry (field, source, value)
712                 VALUES (ind_data.field, ind_data.source, ind_data.value);
713         ELSIF ind_data.field_class = 'subject' THEN
714             INSERT INTO metabib.subject_field_entry (field, source, value)
715                 VALUES (ind_data.field, ind_data.source, ind_data.value);
716         ELSIF ind_data.field_class = 'keyword' THEN
717             INSERT INTO metabib.keyword_field_entry (field, source, value)
718                 VALUES (ind_data.field, ind_data.source, ind_data.value);
719         ELSIF ind_data.field_class = 'series' THEN
720             INSERT INTO metabib.series_field_entry (field, source, value)
721                 VALUES (ind_data.field, ind_data.source, ind_data.value);
722         END IF;
723     END LOOP;
724
725     -- Then, the rec_descriptor
726     INSERT INTO metabib.rec_descriptor (record, item_type, item_form, bib_level, control_type, enc_level, audience, lit_form, type_mat, cat_form, pub_status, item_lang, vr_format, date1, date2)
727         SELECT  NEW.id,
728                 biblio.marc21_extract_fixed_field( NEW.id, 'Type' ),
729                 biblio.marc21_extract_fixed_field( NEW.id, 'Form' ),
730                 biblio.marc21_extract_fixed_field( NEW.id, 'BLvl' ),
731                 biblio.marc21_extract_fixed_field( NEW.id, 'Ctrl' ),
732                 biblio.marc21_extract_fixed_field( NEW.id, 'ELvl' ),
733                 biblio.marc21_extract_fixed_field( NEW.id, 'Audn' ),
734                 biblio.marc21_extract_fixed_field( NEW.id, 'LitF' ),
735                 biblio.marc21_extract_fixed_field( NEW.id, 'TMat' ),
736                 biblio.marc21_extract_fixed_field( NEW.id, 'Desc' ),
737                 biblio.marc21_extract_fixed_field( NEW.id, 'DtSt' ),
738                 biblio.marc21_extract_fixed_field( NEW.id, 'Lang' ),
739                 (   SELECT  v.value
740                       FROM  biblio.marc21_physical_characteristics( NEW.id) p
741                             JOIN config.marc21_physical_characteristic_subfield_map s ON (s.id = p.subfield)
742                             JOIN config.marc21_physical_characteristic_value_map v ON (v.id = p.value)
743                       WHERE p.ptype = 'v' AND s.subfield = 'e'    ),
744                 biblio.marc21_extract_fixed_field( NEW.id, 'Date1'),
745                 biblio.marc21_extract_fixed_field( NEW.id, 'Date2');
746
747     -- On to URIs ...
748     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',NEW.marc);
749     IF ARRAY_UPPER(uris,1) > 0 THEN
750         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
751             -- First we pull infot out of the 856
752             uri_xml     := uris[i];
753
754             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
755             CONTINUE WHEN uri_href IS NULL;
756
757             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()|//*[@code="u"]/text()',uri_xml))[1];
758             CONTINUE WHEN uri_label IS NULL;
759
760             uri_owner   := (oils_xpath('//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',uri_xml))[1];
761             CONTINUE WHEN uri_owner IS NULL;
762     
763             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
764
765             uri_owner := REGEXP_REPLACE(uri_owner, $re$^.*?\((\w+)\).*$$re$, E'\\1');
766     
767             SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
768             CONTINUE WHEN NOT FOUND;
769     
770             -- now we look for a matching uri
771             SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
772             IF NOT FOUND THEN -- create one
773                 INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
774                 SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
775             END IF;
776     
777             -- we need a call number to link through
778             SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = NEW.id AND label = '##URI##' AND NOT deleted;
779             IF NOT FOUND THEN
780                 INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
781                     VALUES (uri_owner_id, NEW.id, 'now', 'now', NEW.editor, NEW.editor, '##URI##');
782                 SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = NEW.id AND label = '##URI##' AND NOT deleted;
783             END IF;
784     
785             -- now, link them if they're not already
786             SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
787             IF NOT FOUND THEN
788                 INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
789             END IF;
790     
791         END LOOP;
792     END IF;
793
794     -- And, finally, metarecord mapping!
795
796     PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
797
798     IF NOT FOUND OR TG_OP = 'UPDATE' THEN
799         FOR tmp_mr IN SELECT  m.* FROM  metabib.metarecord m JOIN metabib.metarecord_source_map s ON (s.metarecord = m.id) WHERE s.source = NEW.id LOOP
800     
801             IF old_mr IS NULL AND NEW.fingerprint = tmp_mr.fingerprint THEN -- Find the first fingerprint-matching
802                 old_mr := tmp_mr.id;
803             ELSE
804                 SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id;
805                 IF source_count = 0 THEN -- No other records
806                     deleted_mrs := ARRAY_APPEND(deleted_mrs, tmp_mr.id);
807                     DELETE FROM metabib.metarecord WHERE id = tmp_mr.id;
808                 END IF;
809             END IF;
810     
811         END LOOP;
812     
813         IF old_mr IS NULL THEN -- we found no suitable, preexisting MR based on old source maps
814             SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = NEW.fingerprint; -- is there one for our current fingerprint?
815             IF old_mr IS NULL THEN -- nope, create one and grab its id
816                 INSERT INTO metabib.metarecord ( fingerprint, master_record ) VALUES ( NEW.fingerprint, NEW.id );
817                 SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = NEW.fingerprint;
818             ELSE -- indeed there is. update it with a null cache and recalcualated master record
819                 UPDATE  metabib.metarecord
820                   SET   mods = NULL,
821                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = NEW.fingerprint ORDER BY quality DESC LIMIT 1)
822                   WHERE id = old_mr;
823             END IF;
824         ELSE -- there was one we already attached to, update its mods cache and master_record
825             UPDATE  metabib.metarecord
826               SET   mods = NULL,
827                     master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = NEW.fingerprint ORDER BY quality DESC LIMIT 1)
828               WHERE id = old_mr;
829         END IF;
830     
831         INSERT INTO metabib.metarecord_source_map (metarecord, source) VALUES (old_mr, NEW.id); -- new source mapping
832     
833         UPDATE action.hold_request SET target = old_mr WHERE target IN ( SELECT explode_array(deleted_mrs) ) AND hold_type = 'M'; -- if we had to delete any MRs above, make sure their holds are moved
834     END IF;
835  
836     RETURN NEW;
837
838 END;
839 $func$ LANGUAGE PLPGSQL;
840
841 COMMIT;