]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/030.schema.metabib.sql
add a table to allow labelling (and eventual expansion) of search classes; index...
[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     curr_text   TEXT;
245     joiner      TEXT := default_joiner; -- XXX will index defs supply a joiner?
246     output_row  metabib.field_entry_template%ROWTYPE;
247 BEGIN
248
249     -- Get the record
250     SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
251
252     -- Loop over the indexing entries
253     FOR idx IN SELECT * FROM config.metabib_field ORDER BY format LOOP
254
255         SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
256
257         -- See if we can skip the XSLT ... it's expensive
258         IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
259             -- Can't skip the transform
260             IF xfrm.xslt <> '---' THEN
261                 transformed_xml := oils_xslt_process(bib.marc,xfrm.xslt);
262             ELSE
263                 transformed_xml := bib.marc;
264             END IF;
265
266             prev_xfrm := xfrm.name;
267         END IF;
268
269         xml_node_list := oils_xpath( idx.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
270
271         raw_text := NULL;
272         FOR xml_node IN SELECT x FROM explode_array(xml_node_list) AS x LOOP
273             CONTINUE WHEN xml_node !~ E'^\\s*<';
274
275             curr_text := ARRAY_TO_STRING(
276                 oils_xpath( '//text()',
277                     REGEXP_REPLACE( -- This escapes all &s not followed by "amp;".  Data ise returned from oils_xpath (above) in UTF-8, not entity encoded
278                         REGEXP_REPLACE( -- This escapes embeded <s
279                             xml_node,
280                             $re$(>[^<]+)(<)([^>]+<)$re$,
281                             E'\\1&lt;\\3',
282                             'g'
283                         ),
284                         '&(?!amp;)',
285                         '&amp;',
286                         'g'
287                     )
288                 ),
289                 ' '
290             );
291
292             CONTINUE WHEN curr_text IS NULL OR curr_text = '';
293
294             IF raw_text IS NOT NULL THEN
295                 raw_text := raw_text || joiner;
296             END IF;
297
298             raw_text := COALESCE(raw_text,'') || curr_text;
299
300             -- insert raw node text for faceting
301             IF idx.facet_field THEN
302
303                 output_row.field_class = idx.field_class;
304                 output_row.field = idx.id;
305                 output_row.source = rid;
306                 output_row.value = BTRIM(REGEXP_REPLACE(curr_text, E'\\s+', ' ', 'g'));
307
308                 RETURN NEXT output_row;
309             END IF;
310
311         END LOOP;
312
313         CONTINUE WHEN raw_text IS NULL OR raw_text = '';
314
315         -- insert combined node text for searching
316         IF idx.search_field THEN
317             output_row.field_class = idx.field_class;
318             output_row.field = idx.id;
319             output_row.source = rid;
320             output_row.value = BTRIM(REGEXP_REPLACE(raw_text, E'\\s+', ' ', 'g'));
321
322             RETURN NEXT output_row;
323         END IF;
324
325     END LOOP;
326
327 END;
328 $func$ LANGUAGE PLPGSQL;
329
330 -- default to a space joiner
331 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( BIGINT ) RETURNS SETOF metabib.field_entry_template AS $func$
332         SELECT * FROM biblio.extract_metabib_field_entry($1, ' ');
333 $func$ LANGUAGE SQL;
334
335 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
336 DECLARE
337         bib     biblio.record_entry%ROWTYPE;
338         output  metabib.full_rec%ROWTYPE;
339         field   RECORD;
340 BEGIN
341         SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
342
343         FOR field IN SELECT * FROM biblio.flatten_marc( bib.marc ) LOOP
344                 output.record := rid;
345                 output.ind1 := field.ind1;
346                 output.ind2 := field.ind2;
347                 output.tag := field.tag;
348                 output.subfield := field.subfield;
349                 IF field.subfield IS NOT NULL AND field.tag NOT IN ('020','022','024') THEN -- exclude standard numbers and control fields
350                         output.value := naco_normalize(field.value, field.subfield);
351                 ELSE
352                         output.value := field.value;
353                 END IF;
354
355                 CONTINUE WHEN output.value IS NULL;
356
357                 RETURN NEXT output;
358         END LOOP;
359 END;
360 $func$ LANGUAGE PLPGSQL;
361
362 /* Old form of biblio.flatten_marc() relied on contrib/xml2 functions that got all crashy in PostgreSQL 8.4 */
363 -- CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT, BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
364 --     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
365 --         UNION
366 --     SELECT  NULL::bigint AS id, NULL::bigint, x.tag::char(3), NULL::TEXT, NULL::TEXT, NULL::TEXT, x.value, NULL::tsvector AS index_vector
367 --       FROM  oils_xpath_table(
368 --                 'id',
369 --                 'marc',
370 --                 'biblio.record_entry',
371 --                 '//*[local-name()="controlfield"]/@tag|//*[local-name()="controlfield"]',
372 --                 'id=' || $2::TEXT
373 --             )x(record int, tag text, value text)
374 --         UNION
375 --     SELECT  NULL::bigint AS id, NULL::bigint, x.tag::char(3), x.ind1, x.ind2, x.subfield, x.value, NULL::tsvector AS index_vector
376 --       FROM  oils_xpath_table(
377 --                 'id',
378 --                 'marc',
379 --                 'biblio.record_entry',
380 --                 '//*[local-name()="datafield"]/@tag|' ||
381 --                 '//*[local-name()="datafield"]/@ind1|' ||
382 --                 '//*[local-name()="datafield"]/@ind2|' ||
383 --                 '//*[local-name()="datafield"]/*/@code|' ||
384 --                 '//*[local-name()="datafield"]/*[@code]',
385 --                 'id=' || $2::TEXT
386 --             )x(record int, tag text, ind1 text, ind2 text, subfield text, value text);
387 -- $func$ LANGUAGE SQL;
388
389 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
390
391 use MARC::Record;
392 use MARC::File::XML (BinaryEncoding => 'UTF-8');
393
394 my $xml = shift;
395 my $r = MARC::Record->new_from_xml( $xml );
396
397 return_next( { tag => 'LDR', value => $r->leader } );
398
399 for my $f ( $r->fields ) {
400         if ($f->is_control_field) {
401                 return_next({ tag => $f->tag, value => $f->data });
402         } else {
403                 for my $s ($f->subfields) {
404                         return_next({
405                                 tag      => $f->tag,
406                                 ind1     => $f->indicator(1),
407                                 ind2     => $f->indicator(2),
408                                 subfield => $s->[0],
409                                 value    => $s->[1]
410                         });
411
412                         if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
413                                 my $trim = $f->indicator(2) || 0;
414                                 return_next({
415                                         tag      => 'tnf',
416                                         ind1     => $f->indicator(1),
417                                         ind2     => $f->indicator(2),
418                                         subfield => 'a',
419                                         value    => substr( $s->[1], $trim )
420                                 });
421                         }
422                 }
423         }
424 }
425
426 return undef;
427
428 $func$ LANGUAGE PLPERLU;
429
430 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
431 DECLARE
432         ldr         RECORD;
433         tval        TEXT;
434         tval_rec    RECORD;
435         bval        TEXT;
436         bval_rec    RECORD;
437     retval      config.marc21_rec_type_map%ROWTYPE;
438 BEGIN
439     SELECT * INTO ldr FROM metabib.full_rec WHERE record = rid AND tag = 'LDR' LIMIT 1;
440
441     IF ldr.id IS NULL THEN
442         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
443         RETURN retval;
444     END IF;
445
446     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
447     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
448
449
450     tval := SUBSTRING( ldr.value, tval_rec.start_pos + 1, tval_rec.length );
451     bval := SUBSTRING( ldr.value, bval_rec.start_pos + 1, bval_rec.length );
452
453     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr.value;
454
455     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
456
457
458     IF retval.code IS NULL THEN
459         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
460     END IF;
461
462     RETURN retval;
463 END;
464 $func$ LANGUAGE PLPGSQL;
465
466 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
467 DECLARE
468     rtype       TEXT;
469     ff_pos      RECORD;
470     tag_data    RECORD;
471     val         TEXT;
472 BEGIN
473     rtype := (biblio.marc21_record_type( rid )).code;
474     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
475         FOR tag_data IN SELECT * FROM metabib.full_rec WHERE tag = UPPER(ff_pos.tag) AND record = rid LOOP
476             val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
477             RETURN val;
478         END LOOP;
479         val := REPEAT( ff_pos.default_val, ff_pos.length );
480         RETURN val;
481     END LOOP;
482
483     RETURN NULL;
484 END;
485 $func$ LANGUAGE PLPGSQL;
486
487 CREATE TYPE biblio.marc21_physical_characteristics AS ( id INT, record BIGINT, ptype TEXT, subfield INT, value INT );
488 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
489 DECLARE
490     rowid   INT := 0;
491     _007    RECORD;
492     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
493     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
494     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
495     retval  biblio.marc21_physical_characteristics%ROWTYPE;
496 BEGIN
497
498     SELECT * INTO _007 FROM metabib.full_rec WHERE record = rid AND tag = '007' LIMIT 1;
499
500     IF _007.id IS NOT NULL THEN
501         SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007.value, 1, 1 );
502
503         IF ptype.ptype_key IS NOT NULL THEN
504             FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
505                 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 );
506
507                 IF pval.id IS NOT NULL THEN
508                     rowid := rowid + 1;
509                     retval.id := rowid;
510                     retval.record := rid;
511                     retval.ptype := ptype.ptype_key;
512                     retval.subfield := psf.id;
513                     retval.value := pval.id;
514                     RETURN NEXT retval;
515                 END IF;
516
517             END LOOP;
518         END IF;
519     END IF;
520
521     RETURN;
522 END;
523 $func$ LANGUAGE PLPGSQL;
524
525 CREATE OR REPLACE FUNCTION biblio.extract_quality ( marc TEXT, best_lang TEXT, best_type TEXT ) RETURNS INT AS $func$
526 DECLARE
527     qual        INT;
528     ldr         TEXT;
529     tval        TEXT;
530     tval_rec    RECORD;
531     bval        TEXT;
532     bval_rec    RECORD;
533     type_map    RECORD;
534     ff_pos      RECORD;
535     ff_tag_data TEXT;
536 BEGIN
537
538     IF marc IS NULL OR marc = '' THEN
539         RETURN NULL;
540     END IF;
541
542     -- First, the count of tags
543     qual := ARRAY_UPPER(oils_xpath('*[local-name()="datafield"]', marc), 1);
544
545     -- now go through a bunch of pain to get the record type
546     IF best_type IS NOT NULL THEN
547         ldr := (oils_xpath('//*[local-name()="leader"]/text()', marc))[1];
548
549         IF ldr IS NOT NULL THEN
550             SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
551             SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
552
553
554             tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
555             bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
556
557             -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
558
559             SELECT * INTO type_map FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
560
561             IF type_map.code IS NOT NULL THEN
562                 IF best_type = type_map.code THEN
563                     qual := qual + qual / 2;
564                 END IF;
565
566                 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
567                     ff_tag_data := SUBSTRING((oils_xpath('//*[@tag="' || ff_pos.tag || '"]/text()',marc))[1], ff_pos.start_pos + 1, ff_pos.length);
568                     IF ff_tag_data = best_lang THEN
569                             qual := qual + 100;
570                     END IF;
571                 END LOOP;
572             END IF;
573         END IF;
574     END IF;
575
576     -- Now look for some quality metrics
577     -- DCL record?
578     IF ARRAY_UPPER(oils_xpath('//*[@tag="040"]/*[@code="a" and contains(.,"DLC")]', marc), 1) = 1 THEN
579         qual := qual + 10;
580     END IF;
581
582     -- From OCLC?
583     IF (oils_xpath('//*[@tag="003"]/text()', marc))[1] ~* E'oclo?c' THEN
584         qual := qual + 10;
585     END IF;
586
587     RETURN qual;
588
589 END;
590 $func$ LANGUAGE PLPGSQL;
591
592 CREATE OR REPLACE FUNCTION biblio.extract_fingerprint ( marc text ) RETURNS TEXT AS $func$
593 DECLARE
594         idx             config.biblio_fingerprint%ROWTYPE;
595         xfrm            config.xml_transform%ROWTYPE;
596         prev_xfrm       TEXT;
597         transformed_xml TEXT;
598         xml_node        TEXT;
599         xml_node_list   TEXT[];
600         raw_text        TEXT;
601     output_text TEXT := '';
602 BEGIN
603
604     IF marc IS NULL OR marc = '' THEN
605         RETURN NULL;
606     END IF;
607
608         -- Loop over the indexing entries
609         FOR idx IN SELECT * FROM config.biblio_fingerprint ORDER BY format, id LOOP
610
611                 SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
612
613                 -- See if we can skip the XSLT ... it's expensive
614                 IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
615                         -- Can't skip the transform
616                         IF xfrm.xslt <> '---' THEN
617                                 transformed_xml := oils_xslt_process(marc,xfrm.xslt);
618                         ELSE
619                                 transformed_xml := marc;
620                         END IF;
621
622                         prev_xfrm := xfrm.name;
623                 END IF;
624
625                 raw_text := COALESCE(
626             naco_normalize(
627                 ARRAY_TO_STRING(
628                     oils_xpath(
629                         '//text()',
630                         (oils_xpath(
631                             idx.xpath,
632                             transformed_xml,
633                             ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] 
634                         ))[1]
635                     ),
636                     ''
637                 )
638             ),
639             ''
640         );
641
642         raw_text := REGEXP_REPLACE(raw_text, E'\\[.+?\\]', E'');
643         raw_text := REGEXP_REPLACE(raw_text, E'\\mthe\\M|\\man?d?d\\M', E'', 'g'); -- arg! the pain!
644
645         IF idx.first_word IS TRUE THEN
646             raw_text := REGEXP_REPLACE(raw_text, E'^(\\w+).*?$', E'\\1');
647         END IF;
648
649                 output_text := output_text || REGEXP_REPLACE(raw_text, E'\\s+', '', 'g');
650
651         END LOOP;
652
653     RETURN output_text;
654
655 END;
656 $func$ LANGUAGE PLPGSQL;
657
658 -- BEFORE UPDATE OR INSERT trigger for biblio.record_entry
659 CREATE OR REPLACE FUNCTION biblio.fingerprint_trigger () RETURNS TRIGGER AS $func$
660 BEGIN
661
662     -- For TG_ARGV, first param is language (like 'eng'), second is record type (like 'BKS')
663
664     IF NEW.deleted IS TRUE THEN -- we don't much care, then, do we?
665         RETURN NEW;
666     END IF;
667
668     NEW.fingerprint := biblio.extract_fingerprint(NEW.marc);
669     NEW.quality := biblio.extract_quality(NEW.marc, TG_ARGV[0], TG_ARGV[1]);
670
671     RETURN NEW;
672
673 END;
674 $func$ LANGUAGE PLPGSQL;
675
676 -- AFTER UPDATE OR INSERT trigger for biblio.record_entry
677 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
678 DECLARE
679     ind_data        metabib.field_entry_template%ROWTYPE;
680     old_mr          INT;
681     tmp_mr          metabib.metarecord%ROWTYPE;
682     source_count    INT;
683     deleted_mrs     INT[];
684     uris            TEXT[];
685     uri_xml         TEXT;
686     uri_label       TEXT;
687     uri_href        TEXT;
688     uri_use         TEXT;
689     uri_owner       TEXT;
690     uri_owner_id    INT;
691     uri_id          INT;
692     uri_cn_id       INT;
693     uri_map_id      INT;
694 BEGIN
695
696     IF NEW.deleted IS TRUE THEN
697         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
698         RETURN NEW; -- and we're done
699     END IF;
700
701     IF TG_OP = 'UPDATE' THEN -- re-ingest?
702         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
703
704         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
705             RETURN NEW;
706         END IF;
707
708         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
709
710     END IF;
711
712     IF TG_OP = 'UPDATE' THEN -- Clean out the cruft
713         DELETE FROM metabib.title_field_entry WHERE source = NEW.id;
714         DELETE FROM metabib.author_field_entry WHERE source = NEW.id;
715         DELETE FROM metabib.subject_field_entry WHERE source = NEW.id;
716         DELETE FROM metabib.keyword_field_entry WHERE source = NEW.id;
717         DELETE FROM metabib.series_field_entry WHERE source = NEW.id;
718         DELETE FROM metabib.full_rec WHERE record = NEW.id;
719         DELETE FROM metabib.rec_descriptor WHERE record = NEW.id;
720
721     END IF;
722
723     -- Shove the flattened MARC in
724     INSERT INTO metabib.full_rec (record, tag, ind1, ind2, subfield, value)
725         SELECT record, tag, ind1, ind2, subfield, value FROM biblio.flatten_marc( NEW.id );
726
727     -- And now the indexing data
728     FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( NEW.id ) LOOP
729         IF ind_data.field_class = 'title' THEN
730             INSERT INTO metabib.title_field_entry (field, source, value)
731                 VALUES (ind_data.field, ind_data.source, ind_data.value);
732         ELSIF ind_data.field_class = 'author' THEN
733             INSERT INTO metabib.author_field_entry (field, source, value)
734                 VALUES (ind_data.field, ind_data.source, ind_data.value);
735         ELSIF ind_data.field_class = 'subject' THEN
736             INSERT INTO metabib.subject_field_entry (field, source, value)
737                 VALUES (ind_data.field, ind_data.source, ind_data.value);
738         ELSIF ind_data.field_class = 'keyword' THEN
739             INSERT INTO metabib.keyword_field_entry (field, source, value)
740                 VALUES (ind_data.field, ind_data.source, ind_data.value);
741         ELSIF ind_data.field_class = 'series' THEN
742             INSERT INTO metabib.series_field_entry (field, source, value)
743                 VALUES (ind_data.field, ind_data.source, ind_data.value);
744         END IF;
745     END LOOP;
746
747     -- Then, the rec_descriptor
748     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)
749         SELECT  NEW.id,
750                 biblio.marc21_extract_fixed_field( NEW.id, 'Type' ),
751                 biblio.marc21_extract_fixed_field( NEW.id, 'Form' ),
752                 biblio.marc21_extract_fixed_field( NEW.id, 'BLvl' ),
753                 biblio.marc21_extract_fixed_field( NEW.id, 'Ctrl' ),
754                 biblio.marc21_extract_fixed_field( NEW.id, 'ELvl' ),
755                 biblio.marc21_extract_fixed_field( NEW.id, 'Audn' ),
756                 biblio.marc21_extract_fixed_field( NEW.id, 'LitF' ),
757                 biblio.marc21_extract_fixed_field( NEW.id, 'TMat' ),
758                 biblio.marc21_extract_fixed_field( NEW.id, 'Desc' ),
759                 biblio.marc21_extract_fixed_field( NEW.id, 'DtSt' ),
760                 biblio.marc21_extract_fixed_field( NEW.id, 'Lang' ),
761                 (   SELECT  v.value
762                       FROM  biblio.marc21_physical_characteristics( NEW.id) p
763                             JOIN config.marc21_physical_characteristic_subfield_map s ON (s.id = p.subfield)
764                             JOIN config.marc21_physical_characteristic_value_map v ON (v.id = p.value)
765                       WHERE p.ptype = 'v' AND s.subfield = 'e'    ),
766                 biblio.marc21_extract_fixed_field( NEW.id, 'Date1'),
767                 biblio.marc21_extract_fixed_field( NEW.id, 'Date2');
768
769     -- On to URIs ...
770     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',NEW.marc);
771     IF ARRAY_UPPER(uris,1) > 0 THEN
772         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
773             -- First we pull infot out of the 856
774             uri_xml     := uris[i];
775
776             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
777             CONTINUE WHEN uri_href IS NULL;
778
779             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()|//*[@code="u"]/text()',uri_xml))[1];
780             CONTINUE WHEN uri_label IS NULL;
781
782             uri_owner   := (oils_xpath('//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',uri_xml))[1];
783             CONTINUE WHEN uri_owner IS NULL;
784     
785             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
786
787             uri_owner := REGEXP_REPLACE(uri_owner, $re$^.*?\((\w+)\).*$$re$, E'\\1');
788     
789             SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
790             CONTINUE WHEN NOT FOUND;
791     
792             -- now we look for a matching uri
793             SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
794             IF NOT FOUND THEN -- create one
795                 INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
796                 SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
797             END IF;
798     
799             -- we need a call number to link through
800             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;
801             IF NOT FOUND THEN
802                 INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
803                     VALUES (uri_owner_id, NEW.id, 'now', 'now', NEW.editor, NEW.editor, '##URI##');
804                 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;
805             END IF;
806     
807             -- now, link them if they're not already
808             SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
809             IF NOT FOUND THEN
810                 INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
811             END IF;
812     
813         END LOOP;
814     END IF;
815
816     -- And, finally, metarecord mapping!
817
818     PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
819
820     IF NOT FOUND OR TG_OP = 'UPDATE' THEN
821         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
822     
823             IF old_mr IS NULL AND NEW.fingerprint = tmp_mr.fingerprint THEN -- Find the first fingerprint-matching
824                 old_mr := tmp_mr.id;
825             ELSE
826                 SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id;
827                 IF source_count = 0 THEN -- No other records
828                     deleted_mrs := ARRAY_APPEND(deleted_mrs, tmp_mr.id);
829                     DELETE FROM metabib.metarecord WHERE id = tmp_mr.id;
830                 END IF;
831             END IF;
832     
833         END LOOP;
834     
835         IF old_mr IS NULL THEN -- we found no suitable, preexisting MR based on old source maps
836             SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = NEW.fingerprint; -- is there one for our current fingerprint?
837             IF old_mr IS NULL THEN -- nope, create one and grab its id
838                 INSERT INTO metabib.metarecord ( fingerprint, master_record ) VALUES ( NEW.fingerprint, NEW.id );
839                 SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = NEW.fingerprint;
840             ELSE -- indeed there is. update it with a null cache and recalcualated master record
841                 UPDATE  metabib.metarecord
842                   SET   mods = NULL,
843                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = NEW.fingerprint ORDER BY quality DESC LIMIT 1)
844                   WHERE id = old_mr;
845             END IF;
846         ELSE -- there was one we already attached to, update its mods cache and master_record
847             UPDATE  metabib.metarecord
848               SET   mods = NULL,
849                     master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = NEW.fingerprint ORDER BY quality DESC LIMIT 1)
850               WHERE id = old_mr;
851         END IF;
852     
853         INSERT INTO metabib.metarecord_source_map (metarecord, source) VALUES (old_mr, NEW.id); -- new source mapping
854     
855         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
856     END IF;
857  
858     RETURN NEW;
859
860 END;
861 $func$ LANGUAGE PLPGSQL;
862
863 COMMIT;