]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/030.schema.metabib.sql
Merge branch 'master' of git.evergreen-ils.org:Evergreen into template-toolkit-opac...
[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 IF EXISTS 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.identifier_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_identifier_field_entry_fti_trigger
40         BEFORE UPDATE OR INSERT ON metabib.identifier_field_entry
41         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('identifier');
42
43 CREATE INDEX metabib_identifier_field_entry_index_vector_idx ON metabib.identifier_field_entry USING GIST (index_vector);
44 CREATE INDEX metabib_identifier_field_entry_value_idx ON metabib.identifier_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
45 CREATE INDEX metabib_identifier_field_entry_source_idx ON metabib.identifier_field_entry (source);
46
47
48 CREATE TABLE metabib.title_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_title_field_entry_fti_trigger
56         BEFORE UPDATE OR INSERT ON metabib.title_field_entry
57         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('title');
58
59 CREATE INDEX metabib_title_field_entry_index_vector_idx ON metabib.title_field_entry USING GIST (index_vector);
60 CREATE INDEX metabib_title_field_entry_value_idx ON metabib.title_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
61 CREATE INDEX metabib_title_field_entry_source_idx ON metabib.title_field_entry (source);
62
63
64 CREATE TABLE metabib.author_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_author_field_entry_fti_trigger
72         BEFORE UPDATE OR INSERT ON metabib.author_field_entry
73         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('author');
74
75 CREATE INDEX metabib_author_field_entry_index_vector_idx ON metabib.author_field_entry USING GIST (index_vector);
76 CREATE INDEX metabib_author_field_entry_value_idx ON metabib.author_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
77 CREATE INDEX metabib_author_field_entry_source_idx ON metabib.author_field_entry (source);
78
79
80 CREATE TABLE metabib.subject_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_subject_field_entry_fti_trigger
88         BEFORE UPDATE OR INSERT ON metabib.subject_field_entry
89         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('subject');
90
91 CREATE INDEX metabib_subject_field_entry_index_vector_idx ON metabib.subject_field_entry USING GIST (index_vector);
92 CREATE INDEX metabib_subject_field_entry_value_idx ON metabib.subject_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
93 CREATE INDEX metabib_subject_field_entry_source_idx ON metabib.subject_field_entry (source);
94
95
96 CREATE TABLE metabib.keyword_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_keyword_field_entry_fti_trigger
104         BEFORE UPDATE OR INSERT ON metabib.keyword_field_entry
105         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
106
107 CREATE INDEX metabib_keyword_field_entry_index_vector_idx ON metabib.keyword_field_entry USING GIST (index_vector);
108 CREATE INDEX metabib_keyword_field_entry_value_idx ON metabib.keyword_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
109 CREATE INDEX metabib_keyword_field_entry_source_idx ON metabib.keyword_field_entry (source);
110
111
112 CREATE TABLE metabib.series_field_entry (
113         id              BIGSERIAL       PRIMARY KEY,
114         source          BIGINT          NOT NULL,
115         field           INT             NOT NULL,
116         value           TEXT            NOT NULL,
117         index_vector    tsvector        NOT NULL
118 );
119 CREATE TRIGGER metabib_series_field_entry_fti_trigger
120         BEFORE UPDATE OR INSERT ON metabib.series_field_entry
121         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('series');
122
123 CREATE INDEX metabib_series_field_entry_index_vector_idx ON metabib.series_field_entry USING GIST (index_vector);
124 CREATE INDEX metabib_series_field_entry_value_idx ON metabib.series_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
125 CREATE INDEX metabib_series_field_entry_source_idx ON metabib.series_field_entry (source);
126
127
128 CREATE TABLE metabib.facet_entry (
129         id              BIGSERIAL       PRIMARY KEY,
130         source          BIGINT          NOT NULL,
131         field           INT             NOT NULL,
132         value           TEXT            NOT NULL
133 );
134 CREATE INDEX metabib_facet_entry_field_idx ON metabib.facet_entry (field);
135 CREATE INDEX metabib_facet_entry_value_idx ON metabib.facet_entry (SUBSTRING(value,1,1024));
136 CREATE INDEX metabib_facet_entry_source_idx ON metabib.facet_entry (source);
137
138 CREATE OR REPLACE FUNCTION metabib.facet_normalize_trigger () RETURNS TRIGGER AS $$
139 DECLARE
140     normalizer  RECORD;
141     facet_text  TEXT;
142 BEGIN
143     facet_text := NEW.value;
144
145     FOR normalizer IN
146         SELECT  n.func AS func,
147                 n.param_count AS param_count,
148                 m.params AS params
149           FROM  config.index_normalizer n
150                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
151           WHERE m.field = NEW.field AND m.pos < 0
152           ORDER BY m.pos LOOP
153
154             EXECUTE 'SELECT ' || normalizer.func || '(' ||
155                 quote_literal( facet_text ) ||
156                 CASE
157                     WHEN normalizer.param_count > 0
158                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
159                         ELSE ''
160                     END ||
161                 ')' INTO facet_text;
162
163     END LOOP;
164
165     NEW.value = facet_text;
166
167     RETURN NEW;
168 END;
169 $$ LANGUAGE PLPGSQL;
170
171 CREATE TRIGGER facet_normalize_tgr
172         BEFORE UPDATE OR INSERT ON metabib.facet_entry
173         FOR EACH ROW EXECUTE PROCEDURE metabib.facet_normalize_trigger();
174
175 CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
176 BEGIN
177     NEW.value := force_unicode_normal_form(NEW.value,'NFC');
178     RETURN NEW;
179 END;
180 $$ LANGUAGE PLPGSQL;
181
182 CREATE TRIGGER facet_force_nfc_tgr
183         BEFORE UPDATE OR INSERT ON metabib.facet_entry
184         FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
185
186 CREATE TABLE metabib.record_attr (
187         id              BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
188         attrs   HSTORE  NOT NULL DEFAULT ''::HSTORE
189 );
190 CREATE INDEX metabib_svf_attrs_idx ON metabib.record_attr USING GIST (attrs);
191 CREATE INDEX metabib_svf_date1_idx ON metabib.record_attr ((attrs->'date1'));
192 CREATE INDEX metabib_svf_dates_idx ON metabib.record_attr ((attrs->'date1'),(attrs->'date2'));
193
194 -- Back-compat view ... we're moving to an HSTORE world
195 CREATE TYPE metabib.rec_desc_type AS (
196     item_type       TEXT,
197     item_form       TEXT,
198     bib_level       TEXT,
199     control_type    TEXT,
200     char_encoding   TEXT,
201     enc_level       TEXT,
202     audience        TEXT,
203     lit_form        TEXT,
204     type_mat        TEXT,
205     cat_form        TEXT,
206     pub_status      TEXT,
207     item_lang       TEXT,
208     vr_format       TEXT,
209     date1           TEXT,
210     date2           TEXT
211 );
212
213 CREATE VIEW metabib.rec_descriptor AS
214     SELECT  id,
215             id AS record,
216             (populate_record(NULL::metabib.rec_desc_type, attrs)).*
217       FROM  metabib.record_attr;
218
219 -- Use a sequence that matches previous version, for easier upgrading.
220 CREATE SEQUENCE metabib.full_rec_id_seq;
221
222 CREATE TABLE metabib.real_full_rec (
223         id                  BIGINT      NOT NULL DEFAULT NEXTVAL('metabib.full_rec_id_seq'::REGCLASS),
224         record          BIGINT          NOT NULL,
225         tag             CHAR(3)         NOT NULL,
226         ind1            TEXT,
227         ind2            TEXT,
228         subfield        TEXT,
229         value           TEXT            NOT NULL,
230         index_vector    tsvector        NOT NULL
231 );
232 ALTER TABLE metabib.real_full_rec ADD PRIMARY KEY (id);
233
234 CREATE INDEX metabib_full_rec_tag_subfield_idx ON metabib.real_full_rec (tag,subfield);
235 CREATE INDEX metabib_full_rec_value_idx ON metabib.real_full_rec (substring(value,1,1024));
236 /* Enable LIKE to use an index for database clusters with locales other than C or POSIX */
237 CREATE INDEX metabib_full_rec_value_tpo_index ON metabib.real_full_rec (substring(value,1,1024) text_pattern_ops);
238 CREATE INDEX metabib_full_rec_record_idx ON metabib.real_full_rec (record);
239 CREATE INDEX metabib_full_rec_index_vector_idx ON metabib.real_full_rec USING GIST (index_vector);
240 CREATE INDEX metabib_full_rec_isxn_caseless_idx
241     ON metabib.real_full_rec (LOWER(value))
242     WHERE tag IN ('020', '022', '024');
243
244
245 CREATE TRIGGER metabib_full_rec_fti_trigger
246         BEFORE UPDATE OR INSERT ON metabib.real_full_rec
247         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('default');
248
249 CREATE OR REPLACE VIEW metabib.full_rec AS
250     SELECT  id,
251             record,
252             tag,
253             ind1,
254             ind2,
255             subfield,
256             SUBSTRING(value,1,1024) AS value,
257             index_vector
258       FROM  metabib.real_full_rec;
259
260 CREATE OR REPLACE RULE metabib_full_rec_insert_rule
261     AS ON INSERT TO metabib.full_rec
262     DO INSTEAD
263     INSERT INTO metabib.real_full_rec VALUES (
264         COALESCE(NEW.id, NEXTVAL('metabib.full_rec_id_seq'::REGCLASS)),
265         NEW.record,
266         NEW.tag,
267         NEW.ind1,
268         NEW.ind2,
269         NEW.subfield,
270         NEW.value,
271         NEW.index_vector
272     );
273
274 CREATE OR REPLACE RULE metabib_full_rec_update_rule
275     AS ON UPDATE TO metabib.full_rec
276     DO INSTEAD
277     UPDATE  metabib.real_full_rec SET
278         id = NEW.id,
279         record = NEW.record,
280         tag = NEW.tag,
281         ind1 = NEW.ind1,
282         ind2 = NEW.ind2,
283         subfield = NEW.subfield,
284         value = NEW.value,
285         index_vector = NEW.index_vector
286       WHERE id = OLD.id;
287
288 CREATE OR REPLACE RULE metabib_full_rec_delete_rule
289     AS ON DELETE TO metabib.full_rec
290     DO INSTEAD
291     DELETE FROM metabib.real_full_rec WHERE id = OLD.id;
292
293 CREATE TABLE metabib.metarecord_source_map (
294         id              BIGSERIAL       PRIMARY KEY,
295         metarecord      BIGINT          NOT NULL,
296         source          BIGINT          NOT NULL
297 );
298 CREATE INDEX metabib_metarecord_source_map_metarecord_idx ON metabib.metarecord_source_map (metarecord);
299 CREATE INDEX metabib_metarecord_source_map_source_record_idx ON metabib.metarecord_source_map (source);
300
301 CREATE TYPE metabib.field_entry_template AS (
302         field_class     TEXT,
303         field           INT,
304         source          BIGINT,
305         value           TEXT
306 );
307
308 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( rid BIGINT, default_joiner TEXT ) RETURNS SETOF metabib.field_entry_template AS $func$
309 DECLARE
310     bib     biblio.record_entry%ROWTYPE;
311     idx     config.metabib_field%ROWTYPE;
312     xfrm        config.xml_transform%ROWTYPE;
313     prev_xfrm   TEXT;
314     transformed_xml TEXT;
315     xml_node    TEXT;
316     xml_node_list   TEXT[];
317     facet_text  TEXT;
318     raw_text    TEXT;
319     curr_text   TEXT;
320     joiner      TEXT := default_joiner; -- XXX will index defs supply a joiner?
321     output_row  metabib.field_entry_template%ROWTYPE;
322 BEGIN
323
324     -- Get the record
325     SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
326
327     -- Loop over the indexing entries
328     FOR idx IN SELECT * FROM config.metabib_field ORDER BY format LOOP
329
330         SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
331
332         -- See if we can skip the XSLT ... it's expensive
333         IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
334             -- Can't skip the transform
335             IF xfrm.xslt <> '---' THEN
336                 transformed_xml := oils_xslt_process(bib.marc,xfrm.xslt);
337             ELSE
338                 transformed_xml := bib.marc;
339             END IF;
340
341             prev_xfrm := xfrm.name;
342         END IF;
343
344         xml_node_list := oils_xpath( idx.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
345
346         raw_text := NULL;
347         FOR xml_node IN SELECT x FROM unnest(xml_node_list) AS x LOOP
348             CONTINUE WHEN xml_node !~ E'^\\s*<';
349
350             curr_text := ARRAY_TO_STRING(
351                 oils_xpath( '//text()',
352                     REGEXP_REPLACE( -- This escapes all &s not followed by "amp;".  Data ise returned from oils_xpath (above) in UTF-8, not entity encoded
353                         REGEXP_REPLACE( -- This escapes embeded <s
354                             xml_node,
355                             $re$(>[^<]+)(<)([^>]+<)$re$,
356                             E'\\1&lt;\\3',
357                             'g'
358                         ),
359                         '&(?!amp;)',
360                         '&amp;',
361                         'g'
362                     )
363                 ),
364                 ' '
365             );
366
367             CONTINUE WHEN curr_text IS NULL OR curr_text = '';
368
369             IF raw_text IS NOT NULL THEN
370                 raw_text := raw_text || joiner;
371             END IF;
372
373             raw_text := COALESCE(raw_text,'') || curr_text;
374
375             -- insert raw node text for faceting
376             IF idx.facet_field THEN
377
378                 IF idx.facet_xpath IS NOT NULL AND idx.facet_xpath <> '' THEN
379                     facet_text := oils_xpath_string( idx.facet_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
380                 ELSE
381                     facet_text := curr_text;
382                 END IF;
383
384                 output_row.field_class = idx.field_class;
385                 output_row.field = -1 * idx.id;
386                 output_row.source = rid;
387                 output_row.value = BTRIM(REGEXP_REPLACE(facet_text, E'\\s+', ' ', 'g'));
388
389                 RETURN NEXT output_row;
390             END IF;
391
392         END LOOP;
393
394         CONTINUE WHEN raw_text IS NULL OR raw_text = '';
395
396         -- insert combined node text for searching
397         IF idx.search_field THEN
398             output_row.field_class = idx.field_class;
399             output_row.field = idx.id;
400             output_row.source = rid;
401             output_row.value = BTRIM(REGEXP_REPLACE(raw_text, E'\\s+', ' ', 'g'));
402
403             RETURN NEXT output_row;
404         END IF;
405
406     END LOOP;
407
408 END;
409 $func$ LANGUAGE PLPGSQL;
410
411 -- default to a space joiner
412 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( BIGINT ) RETURNS SETOF metabib.field_entry_template AS $func$
413         SELECT * FROM biblio.extract_metabib_field_entry($1, ' ');
414 $func$ LANGUAGE SQL;
415
416 CREATE OR REPLACE FUNCTION authority.flatten_marc ( rid BIGINT ) RETURNS SETOF authority.full_rec AS $func$
417 DECLARE
418         auth    authority.record_entry%ROWTYPE;
419         output  authority.full_rec%ROWTYPE;
420         field   RECORD;
421 BEGIN
422         SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
423
424         FOR field IN SELECT * FROM vandelay.flatten_marc( auth.marc ) LOOP
425                 output.record := rid;
426                 output.ind1 := field.ind1;
427                 output.ind2 := field.ind2;
428                 output.tag := field.tag;
429                 output.subfield := field.subfield;
430                 output.value := field.value;
431
432                 RETURN NEXT output;
433         END LOOP;
434 END;
435 $func$ LANGUAGE PLPGSQL;
436
437 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
438 DECLARE
439         bib     biblio.record_entry%ROWTYPE;
440         output  metabib.full_rec%ROWTYPE;
441         field   RECORD;
442 BEGIN
443         SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
444
445         FOR field IN SELECT * FROM vandelay.flatten_marc( bib.marc ) LOOP
446                 output.record := rid;
447                 output.ind1 := field.ind1;
448                 output.ind2 := field.ind2;
449                 output.tag := field.tag;
450                 output.subfield := field.subfield;
451                 output.value := field.value;
452
453                 RETURN NEXT output;
454         END LOOP;
455 END;
456 $func$ LANGUAGE PLPGSQL;
457
458 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
459 DECLARE
460         ldr         TEXT;
461         tval        TEXT;
462         tval_rec    RECORD;
463         bval        TEXT;
464         bval_rec    RECORD;
465     retval      config.marc21_rec_type_map%ROWTYPE;
466 BEGIN
467     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
468
469     IF ldr IS NULL OR ldr = '' THEN
470         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
471         RETURN retval;
472     END IF;
473
474     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
475     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
476
477
478     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
479     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
480
481     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
482
483     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
484
485
486     IF retval.code IS NULL THEN
487         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
488     END IF;
489
490     RETURN retval;
491 END;
492 $func$ LANGUAGE PLPGSQL;
493
494 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
495     SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
496 $func$ LANGUAGE SQL;
497
498 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
499 DECLARE
500     rtype       TEXT;
501     ff_pos      RECORD;
502     tag_data    RECORD;
503     val         TEXT;
504 BEGIN
505     rtype := (vandelay.marc21_record_type( marc )).code;
506     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
507         IF ff_pos.tag = 'ldr' THEN
508             val := oils_xpath_string('//*[local-name()="leader"]', marc);
509             IF val IS NOT NULL THEN
510                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
511                 RETURN val;
512             END IF;
513         ELSE
514             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
515                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
516                 RETURN val;
517             END LOOP;
518         END IF;
519         val := REPEAT( ff_pos.default_val, ff_pos.length );
520         RETURN val;
521     END LOOP;
522
523     RETURN NULL;
524 END;
525 $func$ LANGUAGE PLPGSQL;
526
527 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
528     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
529 $func$ LANGUAGE SQL;
530
531 -- CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
532 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
533 DECLARE
534     tag_data    TEXT;
535     rtype       TEXT;
536     ff_pos      RECORD;
537     output      biblio.record_ff_map%ROWTYPE;
538 BEGIN
539     rtype := (vandelay.marc21_record_type( marc )).code;
540
541     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
542         output.ff_name  := ff_pos.fixed_field;
543         output.ff_value := NULL;
544
545         IF ff_pos.tag = 'ldr' THEN
546             output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
547             IF output.ff_value IS NOT NULL THEN
548                 output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
549                 RETURN NEXT output;
550                 output.ff_value := NULL;
551             END IF;
552         ELSE
553             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
554                 output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
555                 IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
556                 RETURN NEXT output;
557                 output.ff_value := NULL;
558             END LOOP;
559         END IF;
560
561     END LOOP;
562
563     RETURN;
564 END;
565 $func$ LANGUAGE PLPGSQL;
566
567 CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
568     SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
569 $func$ LANGUAGE SQL;
570
571 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
572     SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
573 $func$ LANGUAGE SQL;
574
575 CREATE OR REPLACE FUNCTION biblio.extract_quality ( marc TEXT, best_lang TEXT, best_type TEXT ) RETURNS INT AS $func$
576 DECLARE
577     qual        INT;
578     ldr         TEXT;
579     tval        TEXT;
580     tval_rec    RECORD;
581     bval        TEXT;
582     bval_rec    RECORD;
583     type_map    RECORD;
584     ff_pos      RECORD;
585     ff_tag_data TEXT;
586 BEGIN
587
588     IF marc IS NULL OR marc = '' THEN
589         RETURN NULL;
590     END IF;
591
592     -- First, the count of tags
593     qual := ARRAY_UPPER(oils_xpath('*[local-name()="datafield"]', marc), 1);
594
595     -- now go through a bunch of pain to get the record type
596     IF best_type IS NOT NULL THEN
597         ldr := (oils_xpath('//*[local-name()="leader"]/text()', marc))[1];
598
599         IF ldr IS NOT NULL THEN
600             SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
601             SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
602
603
604             tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
605             bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
606
607             -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
608
609             SELECT * INTO type_map FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
610
611             IF type_map.code IS NOT NULL THEN
612                 IF best_type = type_map.code THEN
613                     qual := qual + qual / 2;
614                 END IF;
615
616                 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
617                     ff_tag_data := SUBSTRING((oils_xpath('//*[@tag="' || ff_pos.tag || '"]/text()',marc))[1], ff_pos.start_pos + 1, ff_pos.length);
618                     IF ff_tag_data = best_lang THEN
619                             qual := qual + 100;
620                     END IF;
621                 END LOOP;
622             END IF;
623         END IF;
624     END IF;
625
626     -- Now look for some quality metrics
627     -- DCL record?
628     IF ARRAY_UPPER(oils_xpath('//*[@tag="040"]/*[@code="a" and contains(.,"DLC")]', marc), 1) = 1 THEN
629         qual := qual + 10;
630     END IF;
631
632     -- From OCLC?
633     IF (oils_xpath('//*[@tag="003"]/text()', marc))[1] ~* E'oclo?c' THEN
634         qual := qual + 10;
635     END IF;
636
637     RETURN qual;
638
639 END;
640 $func$ LANGUAGE PLPGSQL;
641
642 CREATE OR REPLACE FUNCTION biblio.extract_fingerprint ( marc text ) RETURNS TEXT AS $func$
643 DECLARE
644         idx             config.biblio_fingerprint%ROWTYPE;
645         xfrm            config.xml_transform%ROWTYPE;
646         prev_xfrm       TEXT;
647         transformed_xml TEXT;
648         xml_node        TEXT;
649         xml_node_list   TEXT[];
650         raw_text        TEXT;
651     output_text TEXT := '';
652 BEGIN
653
654     IF marc IS NULL OR marc = '' THEN
655         RETURN NULL;
656     END IF;
657
658         -- Loop over the indexing entries
659         FOR idx IN SELECT * FROM config.biblio_fingerprint ORDER BY format, id LOOP
660
661                 SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
662
663                 -- See if we can skip the XSLT ... it's expensive
664                 IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
665                         -- Can't skip the transform
666                         IF xfrm.xslt <> '---' THEN
667                                 transformed_xml := oils_xslt_process(marc,xfrm.xslt);
668                         ELSE
669                                 transformed_xml := marc;
670                         END IF;
671
672                         prev_xfrm := xfrm.name;
673                 END IF;
674
675                 raw_text := COALESCE(
676             naco_normalize(
677                 ARRAY_TO_STRING(
678                     oils_xpath(
679                         '//text()',
680                         (oils_xpath(
681                             idx.xpath,
682                             transformed_xml,
683                             ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] 
684                         ))[1]
685                     ),
686                     ''
687                 )
688             ),
689             ''
690         );
691
692         raw_text := REGEXP_REPLACE(raw_text, E'\\[.+?\\]', E'');
693         raw_text := REGEXP_REPLACE(raw_text, E'\\mthe\\M|\\man?d?d\\M', E'', 'g'); -- arg! the pain!
694
695         IF idx.first_word IS TRUE THEN
696             raw_text := REGEXP_REPLACE(raw_text, E'^(\\w+).*?$', E'\\1');
697         END IF;
698
699                 output_text := output_text || REGEXP_REPLACE(raw_text, E'\\s+', '', 'g');
700
701         END LOOP;
702
703     RETURN output_text;
704
705 END;
706 $func$ LANGUAGE PLPGSQL;
707
708 -- BEFORE UPDATE OR INSERT trigger for biblio.record_entry
709 CREATE OR REPLACE FUNCTION biblio.fingerprint_trigger () RETURNS TRIGGER AS $func$
710 BEGIN
711
712     -- For TG_ARGV, first param is language (like 'eng'), second is record type (like 'BKS')
713
714     IF NEW.deleted IS TRUE THEN -- we don't much care, then, do we?
715         RETURN NEW;
716     END IF;
717
718     NEW.fingerprint := biblio.extract_fingerprint(NEW.marc);
719     NEW.quality := biblio.extract_quality(NEW.marc, TG_ARGV[0], TG_ARGV[1]);
720
721     RETURN NEW;
722
723 END;
724 $func$ LANGUAGE PLPGSQL;
725
726 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_full_rec( bib_id BIGINT ) RETURNS VOID AS $func$
727 BEGIN
728     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
729     IF NOT FOUND THEN
730         DELETE FROM metabib.real_full_rec WHERE record = bib_id;
731     END IF;
732     INSERT INTO metabib.real_full_rec (record, tag, ind1, ind2, subfield, value)
733         SELECT record, tag, ind1, ind2, subfield, value FROM biblio.flatten_marc( bib_id );
734
735     RETURN;
736 END;
737 $func$ LANGUAGE PLPGSQL;
738
739 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_field_entries( bib_id BIGINT ) RETURNS VOID AS $func$
740 DECLARE
741     fclass          RECORD;
742     ind_data        metabib.field_entry_template%ROWTYPE;
743 BEGIN
744     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
745     IF NOT FOUND THEN
746         FOR fclass IN SELECT * FROM config.metabib_class LOOP
747             -- RAISE NOTICE 'Emptying out %', fclass.name;
748             EXECUTE $$DELETE FROM metabib.$$ || fclass.name || $$_field_entry WHERE source = $$ || bib_id;
749         END LOOP;
750         DELETE FROM metabib.facet_entry WHERE source = bib_id;
751     END IF;
752
753     FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( bib_id ) LOOP
754         IF ind_data.field < 0 THEN
755             ind_data.field = -1 * ind_data.field;
756             INSERT INTO metabib.facet_entry (field, source, value)
757                 VALUES (ind_data.field, ind_data.source, ind_data.value);
758         ELSE
759             EXECUTE $$
760                 INSERT INTO metabib.$$ || ind_data.field_class || $$_field_entry (field, source, value)
761                     VALUES ($$ ||
762                         quote_literal(ind_data.field) || $$, $$ ||
763                         quote_literal(ind_data.source) || $$, $$ ||
764                         quote_literal(ind_data.value) ||
765                     $$);$$;
766         END IF;
767
768     END LOOP;
769
770     RETURN;
771 END;
772 $func$ LANGUAGE PLPGSQL;
773
774 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
775 DECLARE
776     uris            TEXT[];
777     uri_xml         TEXT;
778     uri_label       TEXT;
779     uri_href        TEXT;
780     uri_use         TEXT;
781     uri_owner_list  TEXT[];
782     uri_owner       TEXT;
783     uri_owner_id    INT;
784     uri_id          INT;
785     uri_cn_id       INT;
786     uri_map_id      INT;
787 BEGIN
788
789     -- Clear any URI mappings and call numbers for this bib.
790     -- This leads to acn / auricnm inflation, but also enables
791     -- old acn/auricnm's to go away and for bibs to be deleted.
792     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
793         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
794         DELETE FROM asset.call_number WHERE id = uri_cn_id;
795     END LOOP;
796
797     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
798     IF ARRAY_UPPER(uris,1) > 0 THEN
799         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
800             -- First we pull info out of the 856
801             uri_xml     := uris[i];
802
803             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
804             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()',uri_xml))[1];
805             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
806
807             IF uri_label IS NULL THEN
808                 uri_label := uri_href;
809             END IF;
810             CONTINUE WHEN uri_href IS NULL;
811
812             -- Get the distinct list of libraries wanting to use 
813             SELECT  ARRAY_ACCUM(
814                         DISTINCT REGEXP_REPLACE(
815                             x,
816                             $re$^.*?\((\w+)\).*$$re$,
817                             E'\\1'
818                         )
819                     ) INTO uri_owner_list
820               FROM  UNNEST(
821                         oils_xpath(
822                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
823                             uri_xml
824                         )
825                     )x;
826
827             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
828
829                 -- look for a matching uri
830                 IF uri_use IS NULL THEN
831                     SELECT id INTO uri_id
832                         FROM asset.uri
833                         WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active
834                         ORDER BY id LIMIT 1;
835                     IF NOT FOUND THEN -- create one
836                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
837                         SELECT id INTO uri_id
838                             FROM asset.uri
839                             WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
840                     END IF;
841                 ELSE
842                     SELECT id INTO uri_id
843                         FROM asset.uri
844                         WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active
845                         ORDER BY id LIMIT 1;
846                     IF NOT FOUND THEN -- create one
847                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
848                         SELECT id INTO uri_id
849                             FROM asset.uri
850                             WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
851                     END IF;
852                 END IF;
853
854                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
855                     uri_owner := uri_owner_list[j];
856
857                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
858                     CONTINUE WHEN NOT FOUND;
859
860                     -- we need a call number to link through
861                     SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = bib_id AND label = '##URI##' AND NOT deleted;
862                     IF NOT FOUND THEN
863                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
864                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
865                         SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = bib_id AND label = '##URI##' AND NOT deleted;
866                     END IF;
867
868                     -- now, link them if they're not already
869                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
870                     IF NOT FOUND THEN
871                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
872                     END IF;
873
874                 END LOOP;
875
876             END IF;
877
878         END LOOP;
879     END IF;
880
881     RETURN;
882 END;
883 $func$ LANGUAGE PLPGSQL;
884
885 CREATE OR REPLACE FUNCTION metabib.remap_metarecord_for_bib( bib_id BIGINT, fp TEXT ) RETURNS BIGINT AS $func$
886 DECLARE
887     source_count    INT;
888     old_mr          BIGINT;
889     tmp_mr          metabib.metarecord%ROWTYPE;
890     deleted_mrs     BIGINT[];
891 BEGIN
892
893     DELETE FROM metabib.metarecord_source_map WHERE source = bib_id; -- Rid ourselves of the search-estimate-killing linkage
894
895     FOR tmp_mr IN SELECT  m.* FROM  metabib.metarecord m JOIN metabib.metarecord_source_map s ON (s.metarecord = m.id) WHERE s.source = bib_id LOOP
896
897         IF old_mr IS NULL AND fp = tmp_mr.fingerprint THEN -- Find the first fingerprint-matching
898             old_mr := tmp_mr.id;
899         ELSE
900             SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id;
901             IF source_count = 0 THEN -- No other records
902                 deleted_mrs := ARRAY_APPEND(deleted_mrs, tmp_mr.id);
903                 DELETE FROM metabib.metarecord WHERE id = tmp_mr.id;
904             END IF;
905         END IF;
906
907     END LOOP;
908
909     IF old_mr IS NULL THEN -- we found no suitable, preexisting MR based on old source maps
910         SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp; -- is there one for our current fingerprint?
911         IF old_mr IS NULL THEN -- nope, create one and grab its id
912             INSERT INTO metabib.metarecord ( fingerprint, master_record ) VALUES ( fp, bib_id );
913             SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp;
914         ELSE -- indeed there is. update it with a null cache and recalcualated master record
915             UPDATE  metabib.metarecord
916               SET   mods = NULL,
917                     master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp ORDER BY quality DESC LIMIT 1)
918               WHERE id = old_mr;
919         END IF;
920     ELSE -- there was one we already attached to, update its mods cache and master_record
921         UPDATE  metabib.metarecord
922           SET   mods = NULL,
923                 master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp ORDER BY quality DESC LIMIT 1)
924           WHERE id = old_mr;
925     END IF;
926
927     INSERT INTO metabib.metarecord_source_map (metarecord, source) VALUES (old_mr, bib_id); -- new source mapping
928
929     IF ARRAY_UPPER(deleted_mrs,1) > 0 THEN
930         UPDATE action.hold_request SET target = old_mr WHERE target IN ( SELECT unnest(deleted_mrs) ) AND hold_type = 'M'; -- if we had to delete any MRs above, make sure their holds are moved
931     END IF;
932
933     RETURN old_mr;
934
935 END;
936 $func$ LANGUAGE PLPGSQL;
937
938 CREATE OR REPLACE FUNCTION biblio.map_authority_linking (bibid BIGINT, marc TEXT) RETURNS BIGINT AS $func$
939     DELETE FROM authority.bib_linking WHERE bib = $1;
940     INSERT INTO authority.bib_linking (bib, authority)
941         SELECT  y.bib,
942                 y.authority
943           FROM (    SELECT  DISTINCT $1 AS bib,
944                             BTRIM(remove_paren_substring(txt))::BIGINT AS authority
945                       FROM  unnest(oils_xpath('//*[@code="0"]/text()',$2)) x(txt)
946                       WHERE BTRIM(remove_paren_substring(txt)) ~ $re$^\d+$$re$
947                 ) y JOIN authority.record_entry r ON r.id = y.authority;
948     SELECT $1;
949 $func$ LANGUAGE SQL;
950
951 -- AFTER UPDATE OR INSERT trigger for biblio.record_entry
952 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
953 DECLARE
954     transformed_xml TEXT;
955     prev_xfrm       TEXT;
956     normalizer      RECORD;
957     xfrm            config.xml_transform%ROWTYPE;
958     attr_value      TEXT;
959     new_attrs       HSTORE := ''::HSTORE;
960     attr_def        config.record_attr_definition%ROWTYPE;
961 BEGIN
962
963     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
964         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
965         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
966         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
967         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
968         RETURN NEW; -- and we're done
969     END IF;
970
971     IF TG_OP = 'UPDATE' THEN -- re-ingest?
972         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
973
974         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
975             RETURN NEW;
976         END IF;
977     END IF;
978
979     -- Record authority linking
980     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
981     IF NOT FOUND THEN
982         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
983     END IF;
984
985     -- Flatten and insert the mfr data
986     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
987     IF NOT FOUND THEN
988         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
989
990         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
991         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
992         IF NOT FOUND THEN
993             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
994
995                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
996                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
997                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
998                       WHERE record = NEW.id
999                             AND tag LIKE attr_def.tag
1000                             AND CASE
1001                                 WHEN attr_def.sf_list IS NOT NULL 
1002                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
1003                                 ELSE TRUE
1004                                 END
1005                       GROUP BY tag
1006                       ORDER BY tag
1007                       LIMIT 1;
1008
1009                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1010                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
1011
1012                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1013
1014                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1015             
1016                     -- See if we can skip the XSLT ... it's expensive
1017                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1018                         -- Can't skip the transform
1019                         IF xfrm.xslt <> '---' THEN
1020                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
1021                         ELSE
1022                             transformed_xml := NEW.marc;
1023                         END IF;
1024             
1025                         prev_xfrm := xfrm.name;
1026                     END IF;
1027
1028                     IF xfrm.name IS NULL THEN
1029                         -- just grab the marcxml (empty) transform
1030                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1031                         prev_xfrm := xfrm.name;
1032                     END IF;
1033
1034                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
1035
1036                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1037                     SELECT  m.value INTO attr_value
1038                       FROM  biblio.marc21_physical_characteristics(NEW.id) v
1039                             JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
1040                       WHERE v.subfield = attr_def.phys_char_sf
1041                       LIMIT 1; -- Just in case ...
1042
1043                 END IF;
1044
1045                 -- apply index normalizers to attr_value
1046                 FOR normalizer IN
1047                     SELECT  n.func AS func,
1048                             n.param_count AS param_count,
1049                             m.params AS params
1050                       FROM  config.index_normalizer n
1051                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1052                       WHERE attr = attr_def.name
1053                       ORDER BY m.pos LOOP
1054                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
1055                             quote_literal( attr_value ) ||
1056                             CASE
1057                                 WHEN normalizer.param_count > 0
1058                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1059                                     ELSE ''
1060                                 END ||
1061                             ')' INTO attr_value;
1062         
1063                 END LOOP;
1064
1065                 -- Add the new value to the hstore
1066                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
1067
1068             END LOOP;
1069
1070             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
1071                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
1072             ELSE
1073                 UPDATE metabib.record_attr SET attrs = new_attrs WHERE id = NEW.id;
1074             END IF;
1075
1076         END IF;
1077     END IF;
1078
1079     -- Gather and insert the field entry data
1080     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
1081
1082     -- Located URI magic
1083     IF TG_OP = 'INSERT' THEN
1084         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1085         IF NOT FOUND THEN
1086             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1087         END IF;
1088     ELSE
1089         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1090         IF NOT FOUND THEN
1091             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1092         END IF;
1093     END IF;
1094
1095     -- (re)map metarecord-bib linking
1096     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
1097         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
1098         IF NOT FOUND THEN
1099             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1100         END IF;
1101     ELSE -- we're doing an update, and we're not deleted, remap
1102         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
1103         IF NOT FOUND THEN
1104             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1105         END IF;
1106     END IF;
1107
1108     RETURN NEW;
1109 END;
1110 $func$ LANGUAGE PLPGSQL;
1111
1112 COMMIT;