]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/030.schema.metabib.sql
LP#1482757: Be sure to remove all orphaned URI
[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 GIN (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 CREATE TABLE metabib.combined_identifier_field_entry (
48         record          BIGINT          NOT NULL,
49         metabib_field           INT             NULL,
50         index_vector    tsvector        NOT NULL
51 );
52 CREATE UNIQUE INDEX metabib_combined_identifier_field_entry_fakepk_idx ON metabib.combined_identifier_field_entry (record, COALESCE(metabib_field::TEXT,''));
53 CREATE INDEX metabib_combined_identifier_field_entry_index_vector_idx ON metabib.combined_identifier_field_entry USING GIN (index_vector);
54 CREATE INDEX metabib_combined_identifier_field_source_idx ON metabib.combined_identifier_field_entry (metabib_field);
55
56 CREATE TABLE metabib.title_field_entry (
57         id              BIGSERIAL       PRIMARY KEY,
58         source          BIGINT          NOT NULL,
59         field           INT             NOT NULL,
60         value           TEXT            NOT NULL,
61         index_vector    tsvector        NOT NULL
62 );
63 CREATE TRIGGER metabib_title_field_entry_fti_trigger
64         BEFORE UPDATE OR INSERT ON metabib.title_field_entry
65         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('title');
66
67 CREATE INDEX metabib_title_field_entry_index_vector_idx ON metabib.title_field_entry USING GIN (index_vector);
68 CREATE INDEX metabib_title_field_entry_value_idx ON metabib.title_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
69 CREATE INDEX metabib_title_field_entry_source_idx ON metabib.title_field_entry (source);
70
71 CREATE TABLE metabib.combined_title_field_entry (
72         record          BIGINT          NOT NULL,
73         metabib_field           INT             NULL,
74         index_vector    tsvector        NOT NULL
75 );
76 CREATE UNIQUE INDEX metabib_combined_title_field_entry_fakepk_idx ON metabib.combined_title_field_entry (record, COALESCE(metabib_field::TEXT,''));
77 CREATE INDEX metabib_combined_title_field_entry_index_vector_idx ON metabib.combined_title_field_entry USING GIN (index_vector);
78 CREATE INDEX metabib_combined_title_field_source_idx ON metabib.combined_title_field_entry (metabib_field);
79
80 CREATE TABLE metabib.author_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_author_field_entry_fti_trigger
88         BEFORE UPDATE OR INSERT ON metabib.author_field_entry
89         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('author');
90
91 CREATE INDEX metabib_author_field_entry_index_vector_idx ON metabib.author_field_entry USING GIN (index_vector);
92 CREATE INDEX metabib_author_field_entry_value_idx ON metabib.author_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
93 CREATE INDEX metabib_author_field_entry_source_idx ON metabib.author_field_entry (source);
94
95 CREATE TABLE metabib.combined_author_field_entry (
96         record          BIGINT          NOT NULL,
97         metabib_field           INT             NULL,
98         index_vector    tsvector        NOT NULL
99 );
100 CREATE UNIQUE INDEX metabib_combined_author_field_entry_fakepk_idx ON metabib.combined_author_field_entry (record, COALESCE(metabib_field::TEXT,''));
101 CREATE INDEX metabib_combined_author_field_entry_index_vector_idx ON metabib.combined_author_field_entry USING GIN (index_vector);
102 CREATE INDEX metabib_combined_author_field_source_idx ON metabib.combined_author_field_entry (metabib_field);
103
104 CREATE TABLE metabib.subject_field_entry (
105         id              BIGSERIAL       PRIMARY KEY,
106         source          BIGINT          NOT NULL,
107         field           INT             NOT NULL,
108         value           TEXT            NOT NULL,
109         index_vector    tsvector        NOT NULL
110 );
111 CREATE TRIGGER metabib_subject_field_entry_fti_trigger
112         BEFORE UPDATE OR INSERT ON metabib.subject_field_entry
113         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('subject');
114
115 CREATE INDEX metabib_subject_field_entry_index_vector_idx ON metabib.subject_field_entry USING GIN (index_vector);
116 CREATE INDEX metabib_subject_field_entry_value_idx ON metabib.subject_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
117 CREATE INDEX metabib_subject_field_entry_source_idx ON metabib.subject_field_entry (source);
118
119 CREATE TABLE metabib.combined_subject_field_entry (
120         record          BIGINT          NOT NULL,
121         metabib_field           INT             NULL,
122         index_vector    tsvector        NOT NULL
123 );
124 CREATE UNIQUE INDEX metabib_combined_subject_field_entry_fakepk_idx ON metabib.combined_subject_field_entry (record, COALESCE(metabib_field::TEXT,''));
125 CREATE INDEX metabib_combined_subject_field_entry_index_vector_idx ON metabib.combined_subject_field_entry USING GIN (index_vector);
126 CREATE INDEX metabib_combined_subject_field_source_idx ON metabib.combined_subject_field_entry (metabib_field);
127
128 CREATE TABLE metabib.keyword_field_entry (
129         id              BIGSERIAL       PRIMARY KEY,
130         source          BIGINT          NOT NULL,
131         field           INT             NOT NULL,
132         value           TEXT            NOT NULL,
133         index_vector    tsvector        NOT NULL
134 );
135 CREATE TRIGGER metabib_keyword_field_entry_fti_trigger
136         BEFORE UPDATE OR INSERT ON metabib.keyword_field_entry
137         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
138
139 CREATE INDEX metabib_keyword_field_entry_index_vector_idx ON metabib.keyword_field_entry USING GIN (index_vector);
140 CREATE INDEX metabib_keyword_field_entry_value_idx ON metabib.keyword_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
141 CREATE INDEX metabib_keyword_field_entry_source_idx ON metabib.keyword_field_entry (source);
142
143 CREATE TABLE metabib.combined_keyword_field_entry (
144         record          BIGINT          NOT NULL,
145         metabib_field           INT             NULL,
146         index_vector    tsvector        NOT NULL
147 );
148 CREATE UNIQUE INDEX metabib_combined_keyword_field_entry_fakepk_idx ON metabib.combined_keyword_field_entry (record, COALESCE(metabib_field::TEXT,''));
149 CREATE INDEX metabib_combined_keyword_field_entry_index_vector_idx ON metabib.combined_keyword_field_entry USING GIN (index_vector);
150 CREATE INDEX metabib_combined_keyword_field_source_idx ON metabib.combined_keyword_field_entry (metabib_field);
151
152 CREATE TABLE metabib.series_field_entry (
153         id              BIGSERIAL       PRIMARY KEY,
154         source          BIGINT          NOT NULL,
155         field           INT             NOT NULL,
156         value           TEXT            NOT NULL,
157         index_vector    tsvector        NOT NULL
158 );
159 CREATE TRIGGER metabib_series_field_entry_fti_trigger
160         BEFORE UPDATE OR INSERT ON metabib.series_field_entry
161         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('series');
162
163 CREATE INDEX metabib_series_field_entry_index_vector_idx ON metabib.series_field_entry USING GIN (index_vector);
164 CREATE INDEX metabib_series_field_entry_value_idx ON metabib.series_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
165 CREATE INDEX metabib_series_field_entry_source_idx ON metabib.series_field_entry (source);
166
167 CREATE TABLE metabib.combined_series_field_entry (
168         record          BIGINT          NOT NULL,
169         metabib_field           INT             NULL,
170         index_vector    tsvector        NOT NULL
171 );
172 CREATE UNIQUE INDEX metabib_combined_series_field_entry_fakepk_idx ON metabib.combined_series_field_entry (record, COALESCE(metabib_field::TEXT,''));
173 CREATE INDEX metabib_combined_series_field_entry_index_vector_idx ON metabib.combined_series_field_entry USING GIN (index_vector);
174 CREATE INDEX metabib_combined_series_field_source_idx ON metabib.combined_series_field_entry (metabib_field);
175
176 CREATE VIEW metabib.combined_all_field_entry AS
177     SELECT * FROM metabib.combined_title_field_entry
178         UNION ALL
179     SELECT * FROM metabib.combined_author_field_entry
180         UNION ALL
181     SELECT * FROM metabib.combined_subject_field_entry
182         UNION ALL
183     SELECT * FROM metabib.combined_keyword_field_entry
184         UNION ALL
185     SELECT * FROM metabib.combined_identifier_field_entry
186         UNION ALL
187     SELECT * FROM metabib.combined_series_field_entry;
188
189 CREATE TABLE metabib.facet_entry (
190         id              BIGSERIAL       PRIMARY KEY,
191         source          BIGINT          NOT NULL,
192         field           INT             NOT NULL,
193         value           TEXT            NOT NULL
194 );
195 CREATE INDEX metabib_facet_entry_field_idx ON metabib.facet_entry (field);
196 CREATE INDEX metabib_facet_entry_value_idx ON metabib.facet_entry (SUBSTRING(value,1,1024));
197 CREATE INDEX metabib_facet_entry_source_idx ON metabib.facet_entry (source);
198
199 CREATE TABLE metabib.display_entry (
200     id      BIGSERIAL  PRIMARY KEY,
201     source  BIGINT     NOT NULL,
202     field   INT        NOT NULL,
203     value   TEXT       NOT NULL
204 );
205
206 CREATE INDEX metabib_display_entry_field_idx 
207     ON metabib.display_entry (field);
208 CREATE INDEX metabib_display_entry_source_idx 
209     ON metabib.display_entry (source);
210
211 CREATE VIEW metabib.flat_display_entry AS
212     /* One row per display entry fleshed with field info */
213     SELECT
214         mde.source,
215         cdfm.name,
216         cdfm.multi,
217         cmf.label,
218         cmf.id AS field,
219         mde.value
220     FROM metabib.display_entry mde
221     JOIN config.metabib_field cmf ON (cmf.id = mde.field)
222     JOIN config.display_field_map cdfm ON (cdfm.field = mde.field)
223 ;
224
225 CREATE VIEW metabib.compressed_display_entry AS
226 /* Like flat_display_entry except values are compressed into 
227    one row per display_field_map and JSON-ified.  */
228     SELECT 
229         source,
230         name,
231         multi,
232         label,
233         field,
234         CASE WHEN multi THEN
235             TO_JSON(ARRAY_AGG(value))
236         ELSE
237             TO_JSON(MIN(value))
238         END AS value
239     FROM metabib.flat_display_entry
240     GROUP BY 1, 2, 3, 4, 5
241 ;
242
243 CREATE VIEW metabib.wide_display_entry AS
244 /* Table-like view of well-known display fields.   
245    This VIEW expands as well-known display fields are added. */
246     SELECT
247         bre.id AS source,
248         COALESCE(mcde_title.value, 'null')::TEXT AS title,
249         COALESCE(mcde_author.value, 'null')::TEXT AS author,
250         COALESCE(mcde_subject_geographic.value, 'null')::TEXT AS subject_geographic,
251         COALESCE(mcde_subject_name.value, 'null')::TEXT AS subject_name,
252         COALESCE(mcde_subject_temporal.value, 'null')::TEXT AS subject_temporal,
253         COALESCE(mcde_subject_topic.value, 'null')::TEXT AS subject_topic,
254         COALESCE(mcde_creators.value, 'null')::TEXT AS creators,
255         COALESCE(mcde_isbn.value, 'null')::TEXT AS isbn,
256         COALESCE(mcde_issn.value, 'null')::TEXT AS issn,
257         COALESCE(mcde_upc.value, 'null')::TEXT AS upc,
258         COALESCE(mcde_tcn.value, 'null')::TEXT AS tcn,
259         COALESCE(mcde_edition.value, 'null')::TEXT AS edition,
260         COALESCE(mcde_physical_description.value, 'null')::TEXT AS physical_description,
261         COALESCE(mcde_publisher.value, 'null')::TEXT AS publisher,
262         COALESCE(mcde_series_title.value, 'null')::TEXT AS series_title,
263         COALESCE(mcde_abstract.value, 'null')::TEXT AS abstract,
264         COALESCE(mcde_toc.value, 'null')::TEXT AS toc,
265         COALESCE(mcde_pubdate.value, 'null')::TEXT AS pubdate,
266         COALESCE(mcde_type_of_resource.value, 'null')::TEXT AS type_of_resource
267     FROM biblio.record_entry bre
268     LEFT JOIN metabib.compressed_display_entry mcde_title
269         ON (bre.id = mcde_title.source AND mcde_title.name = 'title')
270     LEFT JOIN metabib.compressed_display_entry mcde_author
271         ON (bre.id = mcde_author.source AND mcde_author.name = 'author')
272     LEFT JOIN metabib.compressed_display_entry mcde_subject
273         ON (bre.id = mcde_subject.source AND mcde_subject.name = 'subject')
274     LEFT JOIN metabib.compressed_display_entry mcde_subject_geographic
275         ON (bre.id = mcde_subject_geographic.source
276             AND mcde_subject_geographic.name = 'subject_geographic')
277     LEFT JOIN metabib.compressed_display_entry mcde_subject_name
278         ON (bre.id = mcde_subject_name.source
279             AND mcde_subject_name.name = 'subject_name')
280     LEFT JOIN metabib.compressed_display_entry mcde_subject_temporal
281         ON (bre.id = mcde_subject_temporal.source
282             AND mcde_subject_temporal.name = 'subject_temporal')
283     LEFT JOIN metabib.compressed_display_entry mcde_subject_topic
284         ON (bre.id = mcde_subject_topic.source
285             AND mcde_subject_topic.name = 'subject_topic')
286     LEFT JOIN metabib.compressed_display_entry mcde_creators
287         ON (bre.id = mcde_creators.source AND mcde_creators.name = 'creators')
288     LEFT JOIN metabib.compressed_display_entry mcde_isbn
289         ON (bre.id = mcde_isbn.source AND mcde_isbn.name = 'isbn')
290     LEFT JOIN metabib.compressed_display_entry mcde_issn
291         ON (bre.id = mcde_issn.source AND mcde_issn.name = 'issn')
292     LEFT JOIN metabib.compressed_display_entry mcde_upc
293         ON (bre.id = mcde_upc.source AND mcde_upc.name = 'upc')
294     LEFT JOIN metabib.compressed_display_entry mcde_tcn
295         ON (bre.id = mcde_tcn.source AND mcde_tcn.name = 'tcn')
296     LEFT JOIN metabib.compressed_display_entry mcde_edition
297         ON (bre.id = mcde_edition.source AND mcde_edition.name = 'edition')
298     LEFT JOIN metabib.compressed_display_entry mcde_physical_description
299         ON (bre.id = mcde_physical_description.source
300             AND mcde_physical_description.name = 'physical_description')
301     LEFT JOIN metabib.compressed_display_entry mcde_publisher
302         ON (bre.id = mcde_publisher.source AND mcde_publisher.name = 'publisher')
303     LEFT JOIN metabib.compressed_display_entry mcde_series_title
304         ON (bre.id = mcde_series_title.source AND mcde_series_title.name = 'series_title')
305     LEFT JOIN metabib.compressed_display_entry mcde_abstract
306         ON (bre.id = mcde_abstract.source AND mcde_abstract.name = 'abstract')
307     LEFT JOIN metabib.compressed_display_entry mcde_toc
308         ON (bre.id = mcde_toc.source AND mcde_toc.name = 'toc')
309     LEFT JOIN metabib.compressed_display_entry mcde_pubdate
310         ON (bre.id = mcde_pubdate.source AND mcde_pubdate.name = 'pubdate')
311     LEFT JOIN metabib.compressed_display_entry mcde_type_of_resource
312         ON (bre.id = mcde_type_of_resource.source
313             AND mcde_type_of_resource.name = 'type_of_resource')
314 ;
315
316 CREATE TABLE metabib.browse_entry (
317     id BIGSERIAL PRIMARY KEY,
318     value TEXT,
319     index_vector tsvector,
320     sort_value  TEXT NOT NULL,
321     UNIQUE(sort_value, value)
322 );
323
324
325 CREATE INDEX browse_entry_sort_value_idx
326     ON metabib.browse_entry USING BTREE (sort_value);
327
328 CREATE INDEX metabib_browse_entry_index_vector_idx ON metabib.browse_entry USING GIN (index_vector);
329 CREATE TRIGGER metabib_browse_entry_fti_trigger
330     BEFORE INSERT OR UPDATE ON metabib.browse_entry
331     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
332
333
334 CREATE TABLE metabib.browse_entry_def_map (
335     id BIGSERIAL PRIMARY KEY,
336     entry BIGINT REFERENCES metabib.browse_entry (id),
337     def INT REFERENCES config.metabib_field (id) ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
338     source BIGINT REFERENCES biblio.record_entry (id),
339     authority BIGINT REFERENCES authority.record_entry (id) ON DELETE SET NULL
340 );
341 CREATE INDEX browse_entry_def_map_def_idx ON metabib.browse_entry_def_map (def);
342 CREATE INDEX browse_entry_def_map_entry_idx ON metabib.browse_entry_def_map (entry);
343 CREATE INDEX browse_entry_def_map_source_idx ON metabib.browse_entry_def_map (source);
344
345 CREATE TABLE metabib.browse_entry_simple_heading_map (
346     id BIGSERIAL PRIMARY KEY,
347     entry BIGINT REFERENCES metabib.browse_entry (id),
348     simple_heading BIGINT REFERENCES authority.simple_heading (id) ON DELETE CASCADE
349 );
350 CREATE INDEX browse_entry_sh_map_entry_idx ON metabib.browse_entry_simple_heading_map (entry);
351 CREATE INDEX browse_entry_sh_map_sh_idx ON metabib.browse_entry_simple_heading_map (simple_heading);
352
353 CREATE OR REPLACE FUNCTION metabib.display_field_normalize_trigger () 
354     RETURNS TRIGGER AS $$
355 DECLARE
356     normalizer  RECORD;
357     display_field_text  TEXT;
358 BEGIN
359     display_field_text := NEW.value;
360
361     FOR normalizer IN
362         SELECT  n.func AS func,
363                 n.param_count AS param_count,
364                 m.params AS params
365           FROM  config.index_normalizer n
366                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
367           WHERE m.field = NEW.field AND m.pos < 0
368           ORDER BY m.pos LOOP
369
370             EXECUTE 'SELECT ' || normalizer.func || '(' ||
371                 quote_literal( display_field_text ) ||
372                 CASE
373                     WHEN normalizer.param_count > 0
374                         THEN ',' || REPLACE(REPLACE(BTRIM(
375                             normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
376                         ELSE ''
377                     END ||
378                 ')' INTO display_field_text;
379
380     END LOOP;
381
382     NEW.value = display_field_text;
383
384     RETURN NEW;
385 END;
386 $$ LANGUAGE PLPGSQL;
387
388 CREATE TRIGGER display_field_normalize_tgr
389         BEFORE UPDATE OR INSERT ON metabib.display_entry
390         FOR EACH ROW EXECUTE PROCEDURE metabib.display_field_normalize_trigger();
391
392 CREATE OR REPLACE FUNCTION evergreen.display_field_force_nfc() 
393     RETURNS TRIGGER AS $$
394 BEGIN
395     NEW.value := force_unicode_normal_form(NEW.value,'NFC');
396     RETURN NEW;
397 END;
398 $$ LANGUAGE PLPGSQL;
399
400 CREATE TRIGGER display_field_force_nfc_tgr
401         BEFORE UPDATE OR INSERT ON metabib.display_entry
402         FOR EACH ROW EXECUTE PROCEDURE evergreen.display_field_force_nfc();
403
404
405 CREATE OR REPLACE FUNCTION metabib.facet_normalize_trigger () RETURNS TRIGGER AS $$
406 DECLARE
407     normalizer  RECORD;
408     facet_text  TEXT;
409 BEGIN
410     facet_text := NEW.value;
411
412     FOR normalizer IN
413         SELECT  n.func AS func,
414                 n.param_count AS param_count,
415                 m.params AS params
416           FROM  config.index_normalizer n
417                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
418           WHERE m.field = NEW.field AND m.pos < 0
419           ORDER BY m.pos LOOP
420
421             EXECUTE 'SELECT ' || normalizer.func || '(' ||
422                 quote_literal( facet_text ) ||
423                 CASE
424                     WHEN normalizer.param_count > 0
425                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
426                         ELSE ''
427                     END ||
428                 ')' INTO facet_text;
429
430     END LOOP;
431
432     NEW.value = facet_text;
433
434     RETURN NEW;
435 END;
436 $$ LANGUAGE PLPGSQL;
437
438 CREATE TRIGGER facet_normalize_tgr
439         BEFORE UPDATE OR INSERT ON metabib.facet_entry
440         FOR EACH ROW EXECUTE PROCEDURE metabib.facet_normalize_trigger();
441
442 CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
443 BEGIN
444     NEW.value := force_unicode_normal_form(NEW.value,'NFC');
445     RETURN NEW;
446 END;
447 $$ LANGUAGE PLPGSQL;
448
449 CREATE TRIGGER facet_force_nfc_tgr
450         BEFORE UPDATE OR INSERT ON metabib.facet_entry
451         FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
452
453 -- DECREMENTING serial starts at -1
454 CREATE SEQUENCE metabib.uncontrolled_record_attr_value_id_seq INCREMENT BY -1;
455
456 CREATE TABLE metabib.uncontrolled_record_attr_value (
457     id      BIGINT  PRIMARY KEY DEFAULT nextval('metabib.uncontrolled_record_attr_value_id_seq'),
458     attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name),
459     value   TEXT    NOT NULL
460 );
461 CREATE UNIQUE INDEX muv_once_idx ON metabib.uncontrolled_record_attr_value (attr,value);
462
463 CREATE VIEW metabib.record_attr_id_map AS
464     SELECT id, attr, value FROM metabib.uncontrolled_record_attr_value
465         UNION
466     SELECT  c.id, c.ctype AS attr, c.code AS value
467       FROM  config.coded_value_map c
468             JOIN config.record_attr_definition d ON (d.name = c.ctype AND NOT d.composite);
469
470 CREATE VIEW metabib.composite_attr_id_map AS
471     SELECT  c.id, c.ctype AS attr, c.code AS value
472       FROM  config.coded_value_map c
473             JOIN config.record_attr_definition d ON (d.name = c.ctype AND d.composite);
474
475 CREATE VIEW metabib.full_attr_id_map AS
476     SELECT id, attr, value FROM metabib.record_attr_id_map
477         UNION
478     SELECT id, attr, value FROM metabib.composite_attr_id_map;
479
480
481 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_init () RETURNS BOOL AS $f$
482     $_SHARED{metabib_compile_composite_attr_cache} = {}
483         if ! exists $_SHARED{metabib_compile_composite_attr_cache};
484     return exists $_SHARED{metabib_compile_composite_attr_cache};
485 $f$ LANGUAGE PLPERLU;
486
487 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_disable () RETURNS BOOL AS $f$
488     delete $_SHARED{metabib_compile_composite_attr_cache};
489     return ! exists $_SHARED{metabib_compile_composite_attr_cache};
490 $f$ LANGUAGE PLPERLU;
491
492 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_cache_invalidate () RETURNS BOOL AS $f$
493     SELECT metabib.compile_composite_attr_cache_disable() AND metabib.compile_composite_attr_cache_init();
494 $f$ LANGUAGE SQL;
495
496 CREATE OR REPLACE FUNCTION metabib.composite_attr_def_cache_inval_tgr () RETURNS TRIGGER AS $f$
497 BEGIN
498     PERFORM metabib.compile_composite_attr_cache_invalidate();
499     RETURN NULL;
500 END;
501 $f$ LANGUAGE PLPGSQL;
502
503 CREATE TRIGGER ccraed_cache_inval_tgr AFTER INSERT OR UPDATE OR DELETE ON config.composite_attr_entry_definition FOR EACH STATEMENT EXECUTE PROCEDURE metabib.composite_attr_def_cache_inval_tgr();
504     
505 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RETURNS query_int AS $func$
506
507     use JSON::XS;
508
509     my $json = shift;
510     my $def = decode_json($json);
511
512     die("Composite attribute definition not supplied") unless $def;
513
514     my $_cache = (exists $_SHARED{metabib_compile_composite_attr_cache}) ? 1 : 0;
515
516     return $_SHARED{metabib_compile_composite_attr_cache}{$json}
517         if ($_cache && $_SHARED{metabib_compile_composite_attr_cache}{$json});
518
519     sub recurse {
520         my $d = shift;
521         my $j = '&';
522         my @list;
523
524         if (ref $d eq 'HASH') { # node or AND
525             if (exists $d->{_attr}) { # it is a node
526                 my $plan = spi_prepare('SELECT * FROM metabib.full_attr_id_map WHERE attr = $1 AND value = $2', qw/TEXT TEXT/);
527                 my $id = spi_exec_prepared(
528                     $plan, {limit => 1}, $d->{_attr}, $d->{_val}
529                 )->{rows}[0]{id};
530                 spi_freeplan($plan);
531                 return $id;
532             } elsif (exists $d->{_not} && scalar(keys(%$d)) == 1) { # it is a NOT
533                 return '!' . recurse($$d{_not});
534             } else { # an AND list
535                 @list = map { recurse($$d{$_}) } sort keys %$d;
536             }
537         } elsif (ref $d eq 'ARRAY') {
538             $j = '|';
539             @list = map { recurse($_) } @$d;
540         }
541
542         @list = grep { defined && $_ ne '' } @list;
543
544         return '(' . join($j,@list) . ')' if @list;
545         return '';
546     }
547
548     my $val = recurse($def) || undef;
549     $_SHARED{metabib_compile_composite_attr_cache}{$json} = $val if $_cache;
550     return $val;
551
552 $func$ IMMUTABLE LANGUAGE plperlu;
553
554 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_id INT ) RETURNS query_int AS $func$
555     SELECT metabib.compile_composite_attr(definition) FROM config.composite_attr_entry_definition WHERE coded_value = $1;
556 $func$ STRICT IMMUTABLE LANGUAGE SQL;
557
558 CREATE TABLE metabib.record_attr_vector_list (
559     source  BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id),
560     vlist   INT[]   NOT NULL -- stores id from ccvm AND murav
561 );
562 CREATE INDEX mrca_vlist_idx ON metabib.record_attr_vector_list USING gin ( vlist gin__int_ops );
563
564 /* This becomes a view, and we do sorters differently ...
565 CREATE TABLE metabib.record_attr (
566         id              BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
567         attrs   HSTORE  NOT NULL DEFAULT ''::HSTORE
568 );
569 CREATE INDEX metabib_svf_attrs_idx ON metabib.record_attr USING GIN (attrs);
570 CREATE INDEX metabib_svf_date1_idx ON metabib.record_attr ((attrs->'date1'));
571 CREATE INDEX metabib_svf_dates_idx ON metabib.record_attr ((attrs->'date1'),(attrs->'date2'));
572 */
573
574 /* ... like this */
575 CREATE TABLE metabib.record_sorter (
576     id      BIGSERIAL   PRIMARY KEY,
577     source  BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
578     attr    TEXT        NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE,
579     value   TEXT        NOT NULL
580 );
581 CREATE INDEX metabib_sorter_source_idx ON metabib.record_sorter (source); -- we may not need one of this or the next ... stats will tell
582 CREATE INDEX metabib_sorter_s_a_idx ON metabib.record_sorter (source, attr);
583 CREATE INDEX metabib_sorter_a_v_idx ON metabib.record_sorter (attr, value);
584
585
586 CREATE TYPE metabib.record_attr_type AS (
587     id      BIGINT,
588     attrs   HSTORE
589 );
590
591 -- Back-compat view ... we're moving to an INTARRAY world
592 CREATE VIEW metabib.record_attr_flat AS
593     SELECT  v.source AS id,
594             m.attr AS attr,
595             m.value AS value
596       FROM  metabib.record_attr_vector_list v
597             LEFT JOIN metabib.uncontrolled_record_attr_value m ON ( m.id = ANY( v.vlist ) )
598         UNION
599     SELECT  v.source AS id,
600             c.ctype AS attr,
601             c.code AS value
602       FROM  metabib.record_attr_vector_list v
603             LEFT JOIN config.coded_value_map c ON ( c.id = ANY( v.vlist ) );
604
605 CREATE VIEW metabib.record_attr AS
606     SELECT  id, HSTORE( ARRAY_AGG( attr ), ARRAY_AGG( value ) ) AS attrs
607       FROM  metabib.record_attr_flat
608       WHERE attr IS NOT NULL
609       GROUP BY 1;
610
611 -- Back-back-compat view ... we use to live in an HSTORE world
612 CREATE TYPE metabib.rec_desc_type AS (
613     item_type       TEXT,
614     item_form       TEXT,
615     bib_level       TEXT,
616     control_type    TEXT,
617     char_encoding   TEXT,
618     enc_level       TEXT,
619     audience        TEXT,
620     lit_form        TEXT,
621     type_mat        TEXT,
622     cat_form        TEXT,
623     pub_status      TEXT,
624     item_lang       TEXT,
625     vr_format       TEXT,
626     date1           TEXT,
627     date2           TEXT
628 );
629
630 CREATE VIEW metabib.rec_descriptor AS
631     SELECT  id,
632             id AS record,
633             (populate_record(NULL::metabib.rec_desc_type, attrs)).*
634       FROM  metabib.record_attr;
635
636 -- Use a sequence that matches previous version, for easier upgrading.
637 CREATE SEQUENCE metabib.full_rec_id_seq;
638
639 CREATE TABLE metabib.real_full_rec (
640         id                  BIGINT      NOT NULL DEFAULT NEXTVAL('metabib.full_rec_id_seq'::REGCLASS),
641         record          BIGINT          NOT NULL,
642         tag             CHAR(3)         NOT NULL,
643         ind1            TEXT,
644         ind2            TEXT,
645         subfield        TEXT,
646         value           TEXT            NOT NULL,
647         index_vector    tsvector        NOT NULL
648 );
649 ALTER TABLE metabib.real_full_rec ADD PRIMARY KEY (id);
650
651 CREATE INDEX metabib_full_rec_tag_subfield_idx ON metabib.real_full_rec (tag,subfield);
652 CREATE INDEX metabib_full_rec_value_idx ON metabib.real_full_rec (substring(value,1,1024));
653 /* Enable LIKE to use an index for database clusters with locales other than C or POSIX */
654 CREATE INDEX metabib_full_rec_value_tpo_index ON metabib.real_full_rec (substring(value,1,1024) text_pattern_ops);
655 CREATE INDEX metabib_full_rec_record_idx ON metabib.real_full_rec (record);
656 CREATE INDEX metabib_full_rec_index_vector_idx ON metabib.real_full_rec USING GIN (index_vector);
657 CREATE INDEX metabib_full_rec_isxn_caseless_idx
658     ON metabib.real_full_rec (LOWER(value))
659     WHERE tag IN ('020', '022', '024');
660 -- This next index might fully supplant the one above, but leaving both for now.
661 -- (they are not too large)
662 -- The reason we need this index is to ensure that the query parser always
663 -- prefers this index over the simpler tag/subfield index, as this greatly
664 -- increases Vandelay overlay speed for these identifiers, especially when
665 -- a record has many of these fields (around > 4-6 seems like the cutoff
666 -- on at least one PG9.1 system)
667 -- A similar index could be added for other fields (e.g. 010), but one should
668 -- leave out the LOWER() in all other cases.
669 -- TODO: verify whether we can discard the non tag/subfield/substring version
670 -- above (metabib_full_rec_isxn_caseless_idx)
671 CREATE INDEX metabib_full_rec_02x_tag_subfield_lower_substring
672     ON metabib.real_full_rec (tag, subfield, LOWER(substring(value, 1, 1024)))
673     WHERE tag IN ('020', '022', '024');
674
675
676 CREATE TRIGGER metabib_full_rec_fti_trigger
677         BEFORE UPDATE OR INSERT ON metabib.real_full_rec
678         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('default');
679
680 CREATE OR REPLACE VIEW metabib.full_rec AS
681     SELECT  id,
682             record,
683             tag,
684             ind1,
685             ind2,
686             subfield,
687             SUBSTRING(value,1,1024) AS value,
688             index_vector
689       FROM  metabib.real_full_rec;
690
691 CREATE OR REPLACE RULE metabib_full_rec_insert_rule
692     AS ON INSERT TO metabib.full_rec
693     DO INSTEAD
694     INSERT INTO metabib.real_full_rec VALUES (
695         COALESCE(NEW.id, NEXTVAL('metabib.full_rec_id_seq'::REGCLASS)),
696         NEW.record,
697         NEW.tag,
698         NEW.ind1,
699         NEW.ind2,
700         NEW.subfield,
701         NEW.value,
702         NEW.index_vector
703     );
704
705 CREATE OR REPLACE RULE metabib_full_rec_update_rule
706     AS ON UPDATE TO metabib.full_rec
707     DO INSTEAD
708     UPDATE  metabib.real_full_rec SET
709         id = NEW.id,
710         record = NEW.record,
711         tag = NEW.tag,
712         ind1 = NEW.ind1,
713         ind2 = NEW.ind2,
714         subfield = NEW.subfield,
715         value = NEW.value,
716         index_vector = NEW.index_vector
717       WHERE id = OLD.id;
718
719 CREATE OR REPLACE RULE metabib_full_rec_delete_rule
720     AS ON DELETE TO metabib.full_rec
721     DO INSTEAD
722     DELETE FROM metabib.real_full_rec WHERE id = OLD.id;
723
724 CREATE TABLE metabib.metarecord_source_map (
725         id              BIGSERIAL       PRIMARY KEY,
726         metarecord      BIGINT          NOT NULL,
727         source          BIGINT          NOT NULL
728 );
729 CREATE INDEX metabib_metarecord_source_map_metarecord_idx ON metabib.metarecord_source_map (metarecord);
730 CREATE INDEX metabib_metarecord_source_map_source_record_idx ON metabib.metarecord_source_map (source);
731
732 CREATE TYPE metabib.field_entry_template AS (
733     field_class         TEXT,
734     field               INT,
735     facet_field         BOOL,
736     display_field       BOOL,
737     search_field        BOOL,
738     browse_field        BOOL,
739     source              BIGINT,
740     value               TEXT,
741     authority           BIGINT,
742     sort_value          TEXT
743 );
744
745 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry (
746     rid BIGINT,
747     default_joiner TEXT,
748     field_types TEXT[],
749     only_fields INT[]
750 ) RETURNS SETOF metabib.field_entry_template AS $func$
751 DECLARE
752     bib     biblio.record_entry%ROWTYPE;
753     idx     config.metabib_field%ROWTYPE;
754     xfrm        config.xml_transform%ROWTYPE;
755     prev_xfrm   TEXT;
756     transformed_xml TEXT;
757     xml_node    TEXT;
758     xml_node_list   TEXT[];
759     facet_text  TEXT;
760     display_text TEXT;
761     browse_text TEXT;
762     sort_value  TEXT;
763     raw_text    TEXT;
764     curr_text   TEXT;
765     joiner      TEXT := default_joiner; -- XXX will index defs supply a joiner?
766     authority_text TEXT;
767     authority_link BIGINT;
768     output_row  metabib.field_entry_template%ROWTYPE;
769     process_idx BOOL;
770 BEGIN
771
772     -- Start out with no field-use bools set
773     output_row.browse_field = FALSE;
774     output_row.facet_field = FALSE;
775     output_row.display_field = FALSE;
776     output_row.search_field = FALSE;
777
778     -- Get the record
779     SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
780
781     -- Loop over the indexing entries
782     FOR idx IN SELECT * FROM config.metabib_field WHERE id = ANY (only_fields) ORDER BY format LOOP
783         CONTINUE WHEN idx.xpath IS NULL OR idx.xpath = ''; -- pure virtual field
784
785         process_idx := FALSE;
786         IF idx.display_field AND 'display' = ANY (field_types) THEN process_idx = TRUE; END IF;
787         IF idx.browse_field AND 'browse' = ANY (field_types) THEN process_idx = TRUE; END IF;
788         IF idx.search_field AND 'search' = ANY (field_types) THEN process_idx = TRUE; END IF;
789         IF idx.facet_field AND 'facet' = ANY (field_types) THEN process_idx = TRUE; END IF;
790         CONTINUE WHEN process_idx = FALSE; -- disabled for all types
791
792         joiner := COALESCE(idx.joiner, default_joiner);
793
794         SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
795
796         -- See if we can skip the XSLT ... it's expensive
797         IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
798             -- Can't skip the transform
799             IF xfrm.xslt <> '---' THEN
800                 transformed_xml := oils_xslt_process(bib.marc,xfrm.xslt);
801             ELSE
802                 transformed_xml := bib.marc;
803             END IF;
804
805             prev_xfrm := xfrm.name;
806         END IF;
807
808         xml_node_list := oils_xpath( idx.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
809
810         raw_text := NULL;
811         FOR xml_node IN SELECT x FROM unnest(xml_node_list) AS x LOOP
812             CONTINUE WHEN xml_node !~ E'^\\s*<';
813
814             -- XXX much of this should be moved into oils_xpath_string...
815             curr_text := ARRAY_TO_STRING(array_remove(array_remove(
816                 oils_xpath( '//text()', -- get the content of all the nodes within the main selected node
817                     REGEXP_REPLACE( xml_node, E'\\s+', ' ', 'g' ) -- Translate adjacent whitespace to a single space
818                 ), ' '), ''),  -- throw away morally empty (bankrupt?) strings
819                 joiner
820             );
821
822             CONTINUE WHEN curr_text IS NULL OR curr_text = '';
823
824             IF raw_text IS NOT NULL THEN
825                 raw_text := raw_text || joiner;
826             END IF;
827
828             raw_text := COALESCE(raw_text,'') || curr_text;
829
830             -- autosuggest/metabib.browse_entry
831             IF idx.browse_field THEN
832
833                 IF idx.browse_xpath IS NOT NULL AND idx.browse_xpath <> '' THEN
834                     browse_text := oils_xpath_string( idx.browse_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
835                 ELSE
836                     browse_text := curr_text;
837                 END IF;
838
839                 IF idx.browse_sort_xpath IS NOT NULL AND
840                     idx.browse_sort_xpath <> '' THEN
841
842                     sort_value := oils_xpath_string(
843                         idx.browse_sort_xpath, xml_node, joiner,
844                         ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
845                     );
846                 ELSE
847                     sort_value := browse_text;
848                 END IF;
849
850                 output_row.field_class = idx.field_class;
851                 output_row.field = idx.id;
852                 output_row.source = rid;
853                 output_row.value = BTRIM(REGEXP_REPLACE(browse_text, E'\\s+', ' ', 'g'));
854                 output_row.sort_value :=
855                     public.naco_normalize(sort_value);
856
857                 output_row.authority := NULL;
858
859                 IF idx.authority_xpath IS NOT NULL AND idx.authority_xpath <> '' THEN
860                     authority_text := oils_xpath_string(
861                         idx.authority_xpath, xml_node, joiner,
862                         ARRAY[
863                             ARRAY[xfrm.prefix, xfrm.namespace_uri],
864                             ARRAY['xlink','http://www.w3.org/1999/xlink']
865                         ]
866                     );
867
868                     IF authority_text ~ '^\d+$' THEN
869                         authority_link := authority_text::BIGINT;
870                         PERFORM * FROM authority.record_entry WHERE id = authority_link;
871                         IF FOUND THEN
872                             output_row.authority := authority_link;
873                         END IF;
874                     END IF;
875
876                 END IF;
877
878                 output_row.browse_field = TRUE;
879                 -- Returning browse rows with search_field = true for search+browse
880                 -- configs allows us to retain granularity of being able to search
881                 -- browse fields with "starts with" type operators (for example, for
882                 -- titles of songs in music albums)
883                 IF idx.search_field THEN
884                     output_row.search_field = TRUE;
885                 END IF;
886                 RETURN NEXT output_row;
887                 output_row.browse_field = FALSE;
888                 output_row.search_field = FALSE;
889                 output_row.sort_value := NULL;
890             END IF;
891
892             -- insert raw node text for faceting
893             IF idx.facet_field THEN
894
895                 IF idx.facet_xpath IS NOT NULL AND idx.facet_xpath <> '' THEN
896                     facet_text := oils_xpath_string( idx.facet_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
897                 ELSE
898                     facet_text := curr_text;
899                 END IF;
900
901                 output_row.field_class = idx.field_class;
902                 output_row.field = -1 * idx.id;
903                 output_row.source = rid;
904                 output_row.value = BTRIM(REGEXP_REPLACE(facet_text, E'\\s+', ' ', 'g'));
905
906                 output_row.facet_field = TRUE;
907                 RETURN NEXT output_row;
908                 output_row.facet_field = FALSE;
909             END IF;
910
911             -- insert raw node text for display
912             IF idx.display_field THEN
913
914                 IF idx.display_xpath IS NOT NULL AND idx.display_xpath <> '' THEN
915                     display_text := oils_xpath_string( idx.display_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
916                 ELSE
917                     display_text := curr_text;
918                 END IF;
919
920                 output_row.field_class = idx.field_class;
921                 output_row.field = -1 * idx.id;
922                 output_row.source = rid;
923                 output_row.value = BTRIM(REGEXP_REPLACE(display_text, E'\\s+', ' ', 'g'));
924
925                 output_row.display_field = TRUE;
926                 RETURN NEXT output_row;
927                 output_row.display_field = FALSE;
928             END IF;
929
930         END LOOP;
931
932         CONTINUE WHEN raw_text IS NULL OR raw_text = '';
933
934         -- insert combined node text for searching
935         IF idx.search_field THEN
936             output_row.field_class = idx.field_class;
937             output_row.field = idx.id;
938             output_row.source = rid;
939             output_row.value = BTRIM(REGEXP_REPLACE(raw_text, E'\\s+', ' ', 'g'));
940
941             output_row.search_field = TRUE;
942             RETURN NEXT output_row;
943             output_row.search_field = FALSE;
944         END IF;
945
946     END LOOP;
947
948 END;
949 $func$ LANGUAGE PLPGSQL;
950
951 CREATE OR REPLACE FUNCTION metabib.update_combined_index_vectors(bib_id BIGINT) RETURNS VOID AS $func$
952 DECLARE
953     rdata       TSVECTOR;
954     vclass      TEXT;
955     vfield      INT;
956     rfields     INT[];
957 BEGIN
958     DELETE FROM metabib.combined_keyword_field_entry WHERE record = bib_id;
959     INSERT INTO metabib.combined_keyword_field_entry(record, metabib_field, index_vector)
960         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
961         FROM metabib.keyword_field_entry WHERE source = bib_id GROUP BY field;
962     INSERT INTO metabib.combined_keyword_field_entry(record, metabib_field, index_vector)
963         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
964         FROM metabib.keyword_field_entry WHERE source = bib_id;
965
966     DELETE FROM metabib.combined_title_field_entry WHERE record = bib_id;
967     INSERT INTO metabib.combined_title_field_entry(record, metabib_field, index_vector)
968         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
969         FROM metabib.title_field_entry WHERE source = bib_id GROUP BY field;
970     INSERT INTO metabib.combined_title_field_entry(record, metabib_field, index_vector)
971         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
972         FROM metabib.title_field_entry WHERE source = bib_id;
973
974     DELETE FROM metabib.combined_author_field_entry WHERE record = bib_id;
975     INSERT INTO metabib.combined_author_field_entry(record, metabib_field, index_vector)
976         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
977         FROM metabib.author_field_entry WHERE source = bib_id GROUP BY field;
978     INSERT INTO metabib.combined_author_field_entry(record, metabib_field, index_vector)
979         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
980         FROM metabib.author_field_entry WHERE source = bib_id;
981
982     DELETE FROM metabib.combined_subject_field_entry WHERE record = bib_id;
983     INSERT INTO metabib.combined_subject_field_entry(record, metabib_field, index_vector)
984         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
985         FROM metabib.subject_field_entry WHERE source = bib_id GROUP BY field;
986     INSERT INTO metabib.combined_subject_field_entry(record, metabib_field, index_vector)
987         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
988         FROM metabib.subject_field_entry WHERE source = bib_id;
989
990     DELETE FROM metabib.combined_series_field_entry WHERE record = bib_id;
991     INSERT INTO metabib.combined_series_field_entry(record, metabib_field, index_vector)
992         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
993         FROM metabib.series_field_entry WHERE source = bib_id GROUP BY field;
994     INSERT INTO metabib.combined_series_field_entry(record, metabib_field, index_vector)
995         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
996         FROM metabib.series_field_entry WHERE source = bib_id;
997
998     DELETE FROM metabib.combined_identifier_field_entry WHERE record = bib_id;
999     INSERT INTO metabib.combined_identifier_field_entry(record, metabib_field, index_vector)
1000         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1001         FROM metabib.identifier_field_entry WHERE source = bib_id GROUP BY field;
1002     INSERT INTO metabib.combined_identifier_field_entry(record, metabib_field, index_vector)
1003         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
1004         FROM metabib.identifier_field_entry WHERE source = bib_id;
1005
1006     -- For each virtual def, gather the data from the combined real field
1007     -- entries and append it to the virtual combined entry.
1008     FOR vfield, rfields IN SELECT virtual, ARRAY_AGG(real)  FROM config.metabib_field_virtual_map GROUP BY virtual LOOP
1009         SELECT  field_class INTO vclass
1010           FROM  config.metabib_field
1011           WHERE id = vfield;
1012
1013         SELECT  string_agg(index_vector::TEXT,' ')::tsvector INTO rdata
1014           FROM  metabib.combined_all_field_entry
1015           WHERE record = bib_id
1016                 AND metabib_field = ANY (rfields);
1017
1018         BEGIN -- I cannot wait for INSERT ON CONFLICT ... 9.5, though
1019             EXECUTE $$
1020                 INSERT INTO metabib.combined_$$ || vclass || $$_field_entry
1021                     (record, metabib_field, index_vector) VALUES ($1, $2, $3)
1022             $$ USING bib_id, vfield, rdata;
1023         EXCEPTION WHEN unique_violation THEN
1024             EXECUTE $$
1025                 UPDATE  metabib.combined_$$ || vclass || $$_field_entry
1026                   SET   index_vector = index_vector || $3
1027                   WHERE record = $1
1028                         AND metabib_field = $2
1029             $$ USING bib_id, vfield, rdata;
1030         WHEN OTHERS THEN
1031             -- ignore and move on
1032         END;
1033     END LOOP;
1034 END;
1035 $func$ LANGUAGE PLPGSQL;
1036
1037 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_field_entries( 
1038     bib_id BIGINT,
1039     skip_facet BOOL DEFAULT FALSE, 
1040     skip_display BOOL DEFAULT FALSE,
1041     skip_browse BOOL DEFAULT FALSE, 
1042     skip_search BOOL DEFAULT FALSE,
1043     only_fields INT[] DEFAULT '{}'::INT[]
1044 ) RETURNS VOID AS $func$
1045 DECLARE
1046     fclass          RECORD;
1047     ind_data        metabib.field_entry_template%ROWTYPE;
1048     mbe_row         metabib.browse_entry%ROWTYPE;
1049     mbe_id          BIGINT;
1050     b_skip_facet    BOOL;
1051     b_skip_display    BOOL;
1052     b_skip_browse   BOOL;
1053     b_skip_search   BOOL;
1054     value_prepped   TEXT;
1055     field_list      INT[] := only_fields;
1056     field_types     TEXT[] := '{}'::TEXT[];
1057 BEGIN
1058
1059     IF field_list = '{}'::INT[] THEN
1060         SELECT ARRAY_AGG(id) INTO field_list FROM config.metabib_field;
1061     END IF;
1062
1063     SELECT COALESCE(NULLIF(skip_facet, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_facet_indexing' AND enabled)) INTO b_skip_facet;
1064     SELECT COALESCE(NULLIF(skip_display, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_display_indexing' AND enabled)) INTO b_skip_display;
1065     SELECT COALESCE(NULLIF(skip_browse, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_browse_indexing' AND enabled)) INTO b_skip_browse;
1066     SELECT COALESCE(NULLIF(skip_search, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_search_indexing' AND enabled)) INTO b_skip_search;
1067
1068     IF NOT b_skip_facet THEN field_types := field_types || '{facet}'; END IF;
1069     IF NOT b_skip_display THEN field_types := field_types || '{display}'; END IF;
1070     IF NOT b_skip_browse THEN field_types := field_types || '{browse}'; END IF;
1071     IF NOT b_skip_search THEN field_types := field_types || '{search}'; END IF;
1072
1073     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
1074     IF NOT FOUND THEN
1075         IF NOT b_skip_search THEN
1076             FOR fclass IN SELECT * FROM config.metabib_class LOOP
1077                 -- RAISE NOTICE 'Emptying out %', fclass.name;
1078                 EXECUTE $$DELETE FROM metabib.$$ || fclass.name || $$_field_entry WHERE source = $$ || bib_id;
1079             END LOOP;
1080         END IF;
1081         IF NOT b_skip_facet THEN
1082             DELETE FROM metabib.facet_entry WHERE source = bib_id;
1083         END IF;
1084         IF NOT b_skip_display THEN
1085             DELETE FROM metabib.display_entry WHERE source = bib_id;
1086         END IF;
1087         IF NOT b_skip_browse THEN
1088             DELETE FROM metabib.browse_entry_def_map WHERE source = bib_id;
1089         END IF;
1090     END IF;
1091
1092     FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( bib_id, ' ', field_types, field_list ) LOOP
1093
1094         -- don't store what has been normalized away
1095         CONTINUE WHEN ind_data.value IS NULL;
1096
1097         IF ind_data.field < 0 THEN
1098             ind_data.field = -1 * ind_data.field;
1099         END IF;
1100
1101         IF ind_data.facet_field AND NOT b_skip_facet THEN
1102             INSERT INTO metabib.facet_entry (field, source, value)
1103                 VALUES (ind_data.field, ind_data.source, ind_data.value);
1104         END IF;
1105
1106         IF ind_data.display_field AND NOT b_skip_display THEN
1107             INSERT INTO metabib.display_entry (field, source, value)
1108                 VALUES (ind_data.field, ind_data.source, ind_data.value);
1109         END IF;
1110
1111
1112         IF ind_data.browse_field AND NOT b_skip_browse THEN
1113             -- A caveat about this SELECT: this should take care of replacing
1114             -- old mbe rows when data changes, but not if normalization (by
1115             -- which I mean specifically the output of
1116             -- evergreen.oils_tsearch2()) changes.  It may or may not be
1117             -- expensive to add a comparison of index_vector to index_vector
1118             -- to the WHERE clause below.
1119
1120             CONTINUE WHEN ind_data.sort_value IS NULL;
1121
1122             value_prepped := metabib.browse_normalize(ind_data.value, ind_data.field);
1123             SELECT INTO mbe_row * FROM metabib.browse_entry
1124                 WHERE value = value_prepped AND sort_value = ind_data.sort_value;
1125
1126             IF FOUND THEN
1127                 mbe_id := mbe_row.id;
1128             ELSE
1129                 INSERT INTO metabib.browse_entry
1130                     ( value, sort_value ) VALUES
1131                     ( value_prepped, ind_data.sort_value );
1132
1133                 mbe_id := CURRVAL('metabib.browse_entry_id_seq'::REGCLASS);
1134             END IF;
1135
1136             INSERT INTO metabib.browse_entry_def_map (entry, def, source, authority)
1137                 VALUES (mbe_id, ind_data.field, ind_data.source, ind_data.authority);
1138         END IF;
1139
1140         IF ind_data.search_field AND NOT b_skip_search THEN
1141             -- Avoid inserting duplicate rows
1142             EXECUTE 'SELECT 1 FROM metabib.' || ind_data.field_class ||
1143                 '_field_entry WHERE field = $1 AND source = $2 AND value = $3'
1144                 INTO mbe_id USING ind_data.field, ind_data.source, ind_data.value;
1145                 -- RAISE NOTICE 'Search for an already matching row returned %', mbe_id;
1146             IF mbe_id IS NULL THEN
1147                 EXECUTE $$
1148                 INSERT INTO metabib.$$ || ind_data.field_class || $$_field_entry (field, source, value)
1149                     VALUES ($$ ||
1150                         quote_literal(ind_data.field) || $$, $$ ||
1151                         quote_literal(ind_data.source) || $$, $$ ||
1152                         quote_literal(ind_data.value) ||
1153                     $$);$$;
1154             END IF;
1155         END IF;
1156
1157     END LOOP;
1158
1159     IF NOT b_skip_search THEN
1160         PERFORM metabib.update_combined_index_vectors(bib_id);
1161     END IF;
1162
1163     RETURN;
1164 END;
1165 $func$ LANGUAGE PLPGSQL;
1166
1167 CREATE OR REPLACE FUNCTION authority.flatten_marc ( rid BIGINT ) RETURNS SETOF authority.full_rec AS $func$
1168 DECLARE
1169         auth    authority.record_entry%ROWTYPE;
1170         output  authority.full_rec%ROWTYPE;
1171         field   RECORD;
1172 BEGIN
1173         SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
1174
1175         FOR field IN SELECT * FROM vandelay.flatten_marc( auth.marc ) LOOP
1176                 output.record := rid;
1177                 output.ind1 := field.ind1;
1178                 output.ind2 := field.ind2;
1179                 output.tag := field.tag;
1180                 output.subfield := field.subfield;
1181                 output.value := field.value;
1182
1183                 RETURN NEXT output;
1184         END LOOP;
1185 END;
1186 $func$ LANGUAGE PLPGSQL;
1187
1188 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
1189 DECLARE
1190         bib     biblio.record_entry%ROWTYPE;
1191         output  metabib.full_rec%ROWTYPE;
1192         field   RECORD;
1193 BEGIN
1194         SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
1195
1196         FOR field IN SELECT * FROM vandelay.flatten_marc( bib.marc ) LOOP
1197                 output.record := rid;
1198                 output.ind1 := field.ind1;
1199                 output.ind2 := field.ind2;
1200                 output.tag := field.tag;
1201                 output.subfield := field.subfield;
1202                 output.value := field.value;
1203
1204                 RETURN NEXT output;
1205         END LOOP;
1206 END;
1207 $func$ LANGUAGE PLPGSQL;
1208
1209 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field_list( rid BIGINT, ff TEXT ) RETURNS TEXT[] AS $func$
1210     SELECT * FROM vandelay.marc21_extract_fixed_field_list( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2, TRUE );
1211 $func$ LANGUAGE SQL;
1212
1213 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
1214     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2, TRUE );
1215 $func$ LANGUAGE SQL;
1216
1217 CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
1218     SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1), TRUE );
1219 $func$ LANGUAGE SQL;
1220
1221 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1222     SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1223 $func$ LANGUAGE SQL;
1224
1225 CREATE OR REPLACE FUNCTION biblio.extract_quality ( marc TEXT, best_lang TEXT, best_type TEXT ) RETURNS INT AS $func$
1226 DECLARE
1227     qual        INT;
1228     ldr         TEXT;
1229     tval        TEXT;
1230     tval_rec    RECORD;
1231     bval        TEXT;
1232     bval_rec    RECORD;
1233     type_map    RECORD;
1234     ff_pos      RECORD;
1235     ff_tag_data TEXT;
1236 BEGIN
1237
1238     IF marc IS NULL OR marc = '' THEN
1239         RETURN NULL;
1240     END IF;
1241
1242     -- First, the count of tags
1243     qual := ARRAY_UPPER(oils_xpath('*[local-name()="datafield"]', marc), 1);
1244
1245     -- now go through a bunch of pain to get the record type
1246     IF best_type IS NOT NULL THEN
1247         ldr := (oils_xpath('//*[local-name()="leader"]/text()', marc))[1];
1248
1249         IF ldr IS NOT NULL THEN
1250             SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
1251             SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
1252
1253
1254             tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1255             bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1256
1257             -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1258
1259             SELECT * INTO type_map FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1260
1261             IF type_map.code IS NOT NULL THEN
1262                 IF best_type = type_map.code THEN
1263                     qual := qual + qual / 2;
1264                 END IF;
1265
1266                 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
1267                     ff_tag_data := SUBSTRING((oils_xpath('//*[@tag="' || ff_pos.tag || '"]/text()',marc))[1], ff_pos.start_pos + 1, ff_pos.length);
1268                     IF ff_tag_data = best_lang THEN
1269                             qual := qual + 100;
1270                     END IF;
1271                 END LOOP;
1272             END IF;
1273         END IF;
1274     END IF;
1275
1276     -- Now look for some quality metrics
1277     -- DCL record?
1278     IF ARRAY_UPPER(oils_xpath('//*[@tag="040"]/*[@code="a" and contains(.,"DLC")]', marc), 1) = 1 THEN
1279         qual := qual + 10;
1280     END IF;
1281
1282     -- From OCLC?
1283     IF (oils_xpath('//*[@tag="003"]/text()', marc))[1] ~* E'oclo?c' THEN
1284         qual := qual + 10;
1285     END IF;
1286
1287     RETURN qual;
1288
1289 END;
1290 $func$ LANGUAGE PLPGSQL;
1291
1292 CREATE OR REPLACE FUNCTION biblio.extract_fingerprint ( marc text ) RETURNS TEXT AS $func$
1293 DECLARE
1294         idx             config.biblio_fingerprint%ROWTYPE;
1295         xfrm            config.xml_transform%ROWTYPE;
1296         prev_xfrm       TEXT;
1297         transformed_xml TEXT;
1298         xml_node        TEXT;
1299         xml_node_list   TEXT[];
1300         raw_text        TEXT;
1301     output_text TEXT := '';
1302 BEGIN
1303
1304     IF marc IS NULL OR marc = '' THEN
1305         RETURN NULL;
1306     END IF;
1307
1308         -- Loop over the indexing entries
1309         FOR idx IN SELECT * FROM config.biblio_fingerprint ORDER BY format, id LOOP
1310
1311                 SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
1312
1313                 -- See if we can skip the XSLT ... it's expensive
1314                 IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1315                         -- Can't skip the transform
1316                         IF xfrm.xslt <> '---' THEN
1317                                 transformed_xml := oils_xslt_process(marc,xfrm.xslt);
1318                         ELSE
1319                                 transformed_xml := marc;
1320                         END IF;
1321
1322                         prev_xfrm := xfrm.name;
1323                 END IF;
1324
1325                 raw_text := COALESCE(
1326             naco_normalize(
1327                 ARRAY_TO_STRING(
1328                     oils_xpath(
1329                         '//text()',
1330                         (oils_xpath(
1331                             idx.xpath,
1332                             transformed_xml,
1333                             ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] 
1334                         ))[1]
1335                     ),
1336                     ''
1337                 )
1338             ),
1339             ''
1340         );
1341
1342         raw_text := REGEXP_REPLACE(raw_text, E'\\[.+?\\]', E'');
1343         raw_text := REGEXP_REPLACE(raw_text, E'\\mthe\\M|\\man?d?d\\M', E'', 'g'); -- arg! the pain!
1344
1345         IF idx.first_word IS TRUE THEN
1346             raw_text := REGEXP_REPLACE(raw_text, E'^(\\w+).*?$', E'\\1');
1347         END IF;
1348
1349                 output_text := output_text || idx.name || ':' ||
1350                                            REGEXP_REPLACE(raw_text, E'\\s+', '', 'g') || ' ';
1351
1352         END LOOP;
1353
1354     RETURN BTRIM(output_text);
1355
1356 END;
1357 $func$ LANGUAGE PLPGSQL;
1358
1359 -- BEFORE UPDATE OR INSERT trigger for biblio.record_entry
1360 CREATE OR REPLACE FUNCTION biblio.fingerprint_trigger () RETURNS TRIGGER AS $func$
1361 BEGIN
1362
1363     -- For TG_ARGV, first param is language (like 'eng'), second is record type (like 'BKS')
1364
1365     IF NEW.deleted IS TRUE THEN -- we don't much care, then, do we?
1366         RETURN NEW;
1367     END IF;
1368
1369     NEW.fingerprint := biblio.extract_fingerprint(NEW.marc);
1370     NEW.quality := biblio.extract_quality(NEW.marc, TG_ARGV[0], TG_ARGV[1]);
1371
1372     RETURN NEW;
1373
1374 END;
1375 $func$ LANGUAGE PLPGSQL;
1376
1377 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_full_rec( bib_id BIGINT ) RETURNS VOID AS $func$
1378 BEGIN
1379     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
1380     IF NOT FOUND THEN
1381         DELETE FROM metabib.real_full_rec WHERE record = bib_id;
1382     END IF;
1383     INSERT INTO metabib.real_full_rec (record, tag, ind1, ind2, subfield, value)
1384         SELECT record, tag, ind1, ind2, subfield, value FROM biblio.flatten_marc( bib_id );
1385
1386     RETURN;
1387 END;
1388 $func$ LANGUAGE PLPGSQL;
1389
1390 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
1391 DECLARE
1392     uris            TEXT[];
1393     uri_xml         TEXT;
1394     uri_label       TEXT;
1395     uri_href        TEXT;
1396     uri_use         TEXT;
1397     uri_owner_list  TEXT[];
1398     uri_owner       TEXT;
1399     uri_owner_id    INT;
1400     uri_id          INT;
1401     uri_cn_id       INT;
1402     uri_map_id      INT;
1403     current_uri     INT;
1404     current_map     INT;
1405     uri_map_count   INT;
1406     current_uri_map_list    INT[];
1407     current_map_owner_list  INT[];
1408     orphaned_uri_list       INT[];
1409 BEGIN
1410
1411     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
1412     IF ARRAY_UPPER(uris,1) > 0 THEN
1413         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
1414             -- First we pull info out of the 856
1415             uri_xml     := uris[i];
1416
1417             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
1418             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()',uri_xml))[1];
1419             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
1420
1421             IF uri_label IS NULL THEN
1422                 uri_label := uri_href;
1423             END IF;
1424             CONTINUE WHEN uri_href IS NULL;
1425
1426             -- Get the distinct list of libraries wanting to use 
1427             SELECT  ARRAY_AGG(
1428                         DISTINCT REGEXP_REPLACE(
1429                             x,
1430                             $re$^.*?\((\w+)\).*$$re$,
1431                             E'\\1'
1432                         )
1433                     ) INTO uri_owner_list
1434               FROM  UNNEST(
1435                         oils_xpath(
1436                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
1437                             uri_xml
1438                         )
1439                     )x;
1440
1441             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
1442
1443                 -- look for a matching uri
1444                 IF uri_use IS NULL THEN
1445                     SELECT id INTO uri_id
1446                         FROM asset.uri
1447                         WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active
1448                         ORDER BY id LIMIT 1;
1449                     IF NOT FOUND THEN -- create one
1450                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
1451                         SELECT id INTO uri_id
1452                             FROM asset.uri
1453                             WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
1454                     END IF;
1455                 ELSE
1456                     SELECT id INTO uri_id
1457                         FROM asset.uri
1458                         WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active
1459                         ORDER BY id LIMIT 1;
1460                     IF NOT FOUND THEN -- create one
1461                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
1462                         SELECT id INTO uri_id
1463                             FROM asset.uri
1464                             WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
1465                     END IF;
1466                 END IF;
1467
1468                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
1469                     uri_owner := uri_owner_list[j];
1470
1471                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = BTRIM(REPLACE(uri_owner,chr(160),''));
1472                     CONTINUE WHEN NOT FOUND;
1473
1474                     -- we need a call number to link through
1475                     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;
1476                     IF NOT FOUND THEN
1477                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
1478                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
1479                         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;
1480                     END IF;
1481
1482                     -- now, link them if they're not already
1483                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
1484                     IF NOT FOUND THEN
1485                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
1486                         SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
1487                     END IF;
1488
1489                     current_uri_map_list := current_uri_map_list || uri_map_id;
1490                     current_map_owner_list := current_map_owner_list || uri_cn_id;
1491
1492                 END LOOP;
1493
1494             END IF;
1495
1496         END LOOP;
1497     END IF;
1498
1499     -- Clear any orphaned URIs, URI mappings and call
1500     -- numbers for this bib that weren't mapped above.
1501     FOR current_map IN
1502         SELECT  m.id
1503           FROM  asset.uri_call_number_map m
1504                 JOIN asset.call_number cn ON (cn.id = m.call_number)
1505           WHERE cn.record = bib_id
1506                 AND cn.label = '##URI##'
1507                 AND NOT cn.deleted
1508                 AND NOT (m.id = ANY (current_uri_map_list))
1509     LOOP
1510         SELECT uri INTO current_uri FROM asset.uri_call_number_map WHERE id = current_map;
1511         DELETE FROM asset.uri_call_number_map WHERE id = current_map;
1512
1513         SELECT COUNT(*) INTO uri_map_count FROM asset.uri_call_number_map WHERE uri = current_uri;
1514         IF uri_map_count = 0 THEN 
1515             DELETE FROM asset.uri WHERE id = current_uri;
1516         END IF;
1517     END LOOP;
1518
1519     DELETE FROM asset.call_number WHERE id IN (
1520         SELECT  id
1521           FROM  asset.call_number
1522           WHERE record = bib_id
1523                 AND label = '##URI##'
1524                 AND NOT deleted
1525                 AND NOT (id = ANY (current_map_owner_list))
1526     );
1527
1528     RETURN;
1529 END;
1530 $func$ LANGUAGE PLPGSQL;
1531
1532 CREATE OR REPLACE FUNCTION metabib.remap_metarecord_for_bib(
1533     bib_id bigint,
1534     fp text,
1535     bib_is_deleted boolean DEFAULT false,
1536     retain_deleted boolean DEFAULT false
1537 ) RETURNS bigint AS $function$
1538 DECLARE
1539     new_mapping     BOOL := TRUE;
1540     source_count    INT;
1541     old_mr          BIGINT;
1542     tmp_mr          metabib.metarecord%ROWTYPE;
1543     deleted_mrs     BIGINT[];
1544 BEGIN
1545
1546     -- We need to make sure we're not a deleted master record of an MR
1547     IF bib_is_deleted THEN
1548         IF NOT retain_deleted THEN -- Go away for any MR that we're master of, unless retained
1549             DELETE FROM metabib.metarecord_source_map WHERE source = bib_id;
1550         END IF;
1551
1552         FOR old_mr IN SELECT id FROM metabib.metarecord WHERE master_record = bib_id LOOP
1553
1554             -- Now, are there any more sources on this MR?
1555             SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = old_mr;
1556
1557             IF source_count = 0 AND NOT retain_deleted THEN -- No other records
1558                 deleted_mrs := ARRAY_APPEND(deleted_mrs, old_mr); -- Just in case...
1559                 DELETE FROM metabib.metarecord WHERE id = old_mr;
1560
1561             ELSE -- indeed there are. Update it with a null cache and recalcualated master record
1562                 UPDATE  metabib.metarecord
1563                   SET   mods = NULL,
1564                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
1565                   WHERE id = old_mr;
1566             END IF;
1567         END LOOP;
1568
1569     ELSE -- insert or update
1570
1571         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
1572
1573             -- Find the first fingerprint-matching
1574             IF old_mr IS NULL AND fp = tmp_mr.fingerprint THEN
1575                 old_mr := tmp_mr.id;
1576                 new_mapping := FALSE;
1577
1578             ELSE -- Our fingerprint changed ... maybe remove the old MR
1579                 DELETE FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id AND source = bib_id; -- remove the old source mapping
1580                 SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id;
1581                 IF source_count = 0 THEN -- No other records
1582                     deleted_mrs := ARRAY_APPEND(deleted_mrs, tmp_mr.id);
1583                     DELETE FROM metabib.metarecord WHERE id = tmp_mr.id;
1584                 END IF;
1585             END IF;
1586
1587         END LOOP;
1588
1589         -- we found no suitable, preexisting MR based on old source maps
1590         IF old_mr IS NULL THEN
1591             SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp; -- is there one for our current fingerprint?
1592
1593             IF old_mr IS NULL THEN -- nope, create one and grab its id
1594                 INSERT INTO metabib.metarecord ( fingerprint, master_record ) VALUES ( fp, bib_id );
1595                 SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp;
1596
1597             ELSE -- indeed there is. update it with a null cache and recalcualated master record
1598                 UPDATE  metabib.metarecord
1599                   SET   mods = NULL,
1600                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
1601                   WHERE id = old_mr;
1602             END IF;
1603
1604         ELSE -- there was one we already attached to, update its mods cache and master_record
1605             UPDATE  metabib.metarecord
1606               SET   mods = NULL,
1607                     master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
1608               WHERE id = old_mr;
1609         END IF;
1610
1611         IF new_mapping THEN
1612             INSERT INTO metabib.metarecord_source_map (metarecord, source) VALUES (old_mr, bib_id); -- new source mapping
1613         END IF;
1614
1615     END IF;
1616
1617     IF ARRAY_UPPER(deleted_mrs,1) > 0 THEN
1618         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
1619     END IF;
1620
1621     RETURN old_mr;
1622
1623 END;
1624 $function$ LANGUAGE plpgsql;
1625
1626 CREATE OR REPLACE FUNCTION biblio.map_authority_linking (bibid BIGINT, marc TEXT) RETURNS BIGINT AS $func$
1627     DELETE FROM authority.bib_linking WHERE bib = $1;
1628     INSERT INTO authority.bib_linking (bib, authority)
1629         SELECT  y.bib,
1630                 y.authority
1631           FROM (    SELECT  DISTINCT $1 AS bib,
1632                             BTRIM(remove_paren_substring(txt))::BIGINT AS authority
1633                       FROM  unnest(oils_xpath('//*[@code="0"]/text()',$2)) x(txt)
1634                       WHERE BTRIM(remove_paren_substring(txt)) ~ $re$^\d+$$re$
1635                 ) y JOIN authority.record_entry r ON r.id = y.authority;
1636     SELECT $1;
1637 $func$ LANGUAGE SQL;
1638
1639 CREATE OR REPLACE FUNCTION metabib.reingest_record_attributes (rid BIGINT, pattr_list TEXT[] DEFAULT NULL, prmarc TEXT DEFAULT NULL, rdeleted BOOL DEFAULT TRUE) RETURNS VOID AS $func$
1640 DECLARE
1641     transformed_xml TEXT;
1642     rmarc           TEXT := prmarc;
1643     tmp_val         TEXT;
1644     prev_xfrm       TEXT;
1645     normalizer      RECORD;
1646     xfrm            config.xml_transform%ROWTYPE;
1647     attr_vector     INT[] := '{}'::INT[];
1648     attr_vector_tmp INT[];
1649     attr_list       TEXT[] := pattr_list;
1650     attr_value      TEXT[];
1651     norm_attr_value TEXT[];
1652     tmp_xml         TEXT;
1653     tmp_array       TEXT[];
1654     attr_def        config.record_attr_definition%ROWTYPE;
1655     ccvm_row        config.coded_value_map%ROWTYPE;
1656     jump_past       BOOL;
1657 BEGIN
1658
1659     IF attr_list IS NULL OR rdeleted THEN -- need to do the full dance on INSERT or undelete
1660         SELECT ARRAY_AGG(name) INTO attr_list FROM config.record_attr_definition
1661         WHERE (
1662             tag IS NOT NULL OR
1663             fixed_field IS NOT NULL OR
1664             xpath IS NOT NULL OR
1665             phys_char_sf IS NOT NULL OR
1666             composite
1667         ) AND (
1668             filter OR sorter
1669         );
1670     END IF;
1671
1672     IF rmarc IS NULL THEN
1673         SELECT marc INTO rmarc FROM biblio.record_entry WHERE id = rid;
1674     END IF;
1675
1676     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE NOT composite AND name = ANY( attr_list ) ORDER BY format LOOP
1677
1678         jump_past := FALSE; -- This gets set when we are non-multi and have found something
1679         attr_value := '{}'::TEXT[];
1680         norm_attr_value := '{}'::TEXT[];
1681         attr_vector_tmp := '{}'::INT[];
1682
1683         SELECT * INTO ccvm_row FROM config.coded_value_map c WHERE c.ctype = attr_def.name LIMIT 1; 
1684
1685         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1686             SELECT  ARRAY_AGG(value) INTO attr_value
1687               FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
1688               WHERE record = rid
1689                     AND tag LIKE attr_def.tag
1690                     AND CASE
1691                         WHEN attr_def.sf_list IS NOT NULL 
1692                             THEN POSITION(subfield IN attr_def.sf_list) > 0
1693                         ELSE TRUE
1694                     END
1695               GROUP BY tag
1696               ORDER BY tag;
1697
1698             IF NOT attr_def.multi THEN
1699                 attr_value := ARRAY[ARRAY_TO_STRING(attr_value, COALESCE(attr_def.joiner,' '))];
1700                 jump_past := TRUE;
1701             END IF;
1702         END IF;
1703
1704         IF NOT jump_past AND attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1705             attr_value := attr_value || vandelay.marc21_extract_fixed_field_list(rmarc, attr_def.fixed_field);
1706
1707             IF NOT attr_def.multi THEN
1708                 attr_value := ARRAY[attr_value[1]];
1709                 jump_past := TRUE;
1710             END IF;
1711         END IF;
1712
1713         IF NOT jump_past AND attr_def.xpath IS NOT NULL THEN -- and xpath expression
1714
1715             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1716         
1717             -- See if we can skip the XSLT ... it's expensive
1718             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1719                 -- Can't skip the transform
1720                 IF xfrm.xslt <> '---' THEN
1721                     transformed_xml := oils_xslt_process(rmarc,xfrm.xslt);
1722                 ELSE
1723                     transformed_xml := rmarc;
1724                 END IF;
1725     
1726                 prev_xfrm := xfrm.name;
1727             END IF;
1728
1729             IF xfrm.name IS NULL THEN
1730                 -- just grab the marcxml (empty) transform
1731                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1732                 prev_xfrm := xfrm.name;
1733             END IF;
1734
1735             FOR tmp_xml IN SELECT UNNEST(oils_xpath(attr_def.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]])) LOOP
1736                 tmp_val := oils_xpath_string(
1737                                 '//*',
1738                                 tmp_xml,
1739                                 COALESCE(attr_def.joiner,' '),
1740                                 ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
1741                             );
1742                 IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
1743                     attr_value := attr_value || tmp_val;
1744                     EXIT WHEN NOT attr_def.multi;
1745                 END IF;
1746             END LOOP;
1747         END IF;
1748
1749         IF NOT jump_past AND attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1750             SELECT  ARRAY_AGG(m.value) INTO tmp_array
1751               FROM  vandelay.marc21_physical_characteristics(rmarc) v
1752                     LEFT JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
1753               WHERE v.subfield = attr_def.phys_char_sf AND (m.value IS NOT NULL AND BTRIM(m.value) <> '')
1754                     AND ( ccvm_row.id IS NULL OR ( ccvm_row.id IS NOT NULL AND v.id IS NOT NULL) );
1755
1756             attr_value := attr_value || tmp_array;
1757
1758             IF NOT attr_def.multi THEN
1759                 attr_value := ARRAY[attr_value[1]];
1760             END IF;
1761
1762         END IF;
1763
1764                 -- apply index normalizers to attr_value
1765         FOR tmp_val IN SELECT value FROM UNNEST(attr_value) x(value) LOOP
1766             FOR normalizer IN
1767                 SELECT  n.func AS func,
1768                         n.param_count AS param_count,
1769                         m.params AS params
1770                   FROM  config.index_normalizer n
1771                         JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1772                   WHERE attr = attr_def.name
1773                   ORDER BY m.pos LOOP
1774                     EXECUTE 'SELECT ' || normalizer.func || '(' ||
1775                     COALESCE( quote_literal( tmp_val ), 'NULL' ) ||
1776                         CASE
1777                             WHEN normalizer.param_count > 0
1778                                 THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1779                                 ELSE ''
1780                             END ||
1781                     ')' INTO tmp_val;
1782
1783             END LOOP;
1784             IF tmp_val IS NOT NULL AND tmp_val <> '' THEN
1785                 -- note that a string that contains only blanks
1786                 -- is a valid value for some attributes
1787                 norm_attr_value := norm_attr_value || tmp_val;
1788             END IF;
1789         END LOOP;
1790         
1791         IF attr_def.filter THEN
1792             -- Create unknown uncontrolled values and find the IDs of the values
1793             IF ccvm_row.id IS NULL THEN
1794                 FOR tmp_val IN SELECT value FROM UNNEST(norm_attr_value) x(value) LOOP
1795                     IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
1796                         BEGIN -- use subtransaction to isolate unique constraint violations
1797                             INSERT INTO metabib.uncontrolled_record_attr_value ( attr, value ) VALUES ( attr_def.name, tmp_val );
1798                         EXCEPTION WHEN unique_violation THEN END;
1799                     END IF;
1800                 END LOOP;
1801
1802                 SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.uncontrolled_record_attr_value WHERE attr = attr_def.name AND value = ANY( norm_attr_value );
1803             ELSE
1804                 SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM config.coded_value_map WHERE ctype = attr_def.name AND code = ANY( norm_attr_value );
1805             END IF;
1806
1807             -- Add the new value to the vector
1808             attr_vector := attr_vector || attr_vector_tmp;
1809         END IF;
1810
1811         IF attr_def.sorter THEN
1812             DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
1813             IF norm_attr_value[1] IS NOT NULL THEN
1814                 INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, norm_attr_value[1]);
1815             END IF;
1816         END IF;
1817
1818     END LOOP;
1819
1820 /* We may need to rewrite the vlist to contain
1821    the intersection of new values for requested
1822    attrs and old values for ignored attrs. To
1823    do this, we take the old attr vlist and
1824    subtract any values that are valid for the
1825    requested attrs, and then add back the new
1826    set of attr values. */
1827
1828     IF ARRAY_LENGTH(pattr_list, 1) > 0 THEN 
1829         SELECT vlist INTO attr_vector_tmp FROM metabib.record_attr_vector_list WHERE source = rid;
1830         SELECT attr_vector_tmp - ARRAY_AGG(id::INT) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
1831         attr_vector := attr_vector || attr_vector_tmp;
1832     END IF;
1833
1834     -- On to composite attributes, now that the record attrs have been pulled.  Processed in name order, so later composite
1835     -- attributes can depend on earlier ones.
1836     PERFORM metabib.compile_composite_attr_cache_init();
1837     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE composite AND name = ANY( attr_list ) ORDER BY name LOOP
1838
1839         FOR ccvm_row IN SELECT * FROM config.coded_value_map c WHERE c.ctype = attr_def.name ORDER BY value LOOP
1840
1841             tmp_val := metabib.compile_composite_attr( ccvm_row.id );
1842             CONTINUE WHEN tmp_val IS NULL OR tmp_val = ''; -- nothing to do
1843
1844             IF attr_def.filter THEN
1845                 IF attr_vector @@ tmp_val::query_int THEN
1846                     attr_vector = attr_vector + intset(ccvm_row.id);
1847                     EXIT WHEN NOT attr_def.multi;
1848                 END IF;
1849             END IF;
1850
1851             IF attr_def.sorter THEN
1852                 IF attr_vector @@ tmp_val THEN
1853                     DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
1854                     INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, ccvm_row.code);
1855                 END IF;
1856             END IF;
1857
1858         END LOOP;
1859
1860     END LOOP;
1861
1862     IF ARRAY_LENGTH(attr_vector, 1) > 0 THEN
1863         IF rdeleted THEN -- initial insert OR revivication
1864             DELETE FROM metabib.record_attr_vector_list WHERE source = rid;
1865             INSERT INTO metabib.record_attr_vector_list (source, vlist) VALUES (rid, attr_vector);
1866         ELSE
1867             UPDATE metabib.record_attr_vector_list SET vlist = attr_vector WHERE source = rid;
1868         END IF;
1869     END IF;
1870
1871 END;
1872
1873 $func$ LANGUAGE PLPGSQL;
1874
1875
1876 -- AFTER UPDATE OR INSERT trigger for biblio.record_entry
1877 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
1878 DECLARE
1879     tmp_bool BOOL;
1880 BEGIN
1881
1882     IF NEW.deleted THEN -- If this bib is deleted
1883
1884         PERFORM * FROM config.internal_flag WHERE
1885             name = 'ingest.metarecord_mapping.preserve_on_delete' AND enabled;
1886
1887         tmp_bool := FOUND; -- Just in case this is changed by some other statement
1888
1889         PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint, TRUE, tmp_bool );
1890
1891         IF NOT tmp_bool THEN
1892             -- One needs to keep these around to support searches
1893             -- with the #deleted modifier, so one should turn on the named
1894             -- internal flag for that functionality.
1895             DELETE FROM metabib.record_attr_vector_list WHERE source = NEW.id;
1896         END IF;
1897
1898         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
1899         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
1900         DELETE FROM metabib.browse_entry_def_map WHERE source = NEW.id; -- Don't auto-suggest deleted bibs
1901         RETURN NEW; -- and we're done
1902     END IF;
1903
1904     IF TG_OP = 'UPDATE' AND OLD.deleted IS FALSE THEN -- re-ingest?
1905         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
1906
1907         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
1908             RETURN NEW;
1909         END IF;
1910     END IF;
1911
1912     -- Record authority linking
1913     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
1914     IF NOT FOUND THEN
1915         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
1916     END IF;
1917
1918     -- Flatten and insert the mfr data
1919     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
1920     IF NOT FOUND THEN
1921         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
1922
1923         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
1924         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
1925         IF NOT FOUND THEN
1926             PERFORM metabib.reingest_record_attributes(NEW.id, NULL, NEW.marc, TG_OP = 'INSERT' OR OLD.deleted);
1927         END IF;
1928     END IF;
1929
1930     -- Gather and insert the field entry data
1931     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
1932
1933     -- Located URI magic
1934     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1935     IF NOT FOUND THEN PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor ); END IF;
1936
1937     -- (re)map metarecord-bib linking
1938     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
1939         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
1940         IF NOT FOUND THEN
1941             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1942         END IF;
1943     ELSE -- we're doing an update, and we're not deleted, remap
1944         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
1945         IF NOT FOUND THEN
1946             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1947         END IF;
1948     END IF;
1949
1950     RETURN NEW;
1951 END;
1952 $func$ LANGUAGE PLPGSQL;
1953
1954 CREATE OR REPLACE FUNCTION metabib.browse_normalize(facet_text TEXT, mapped_field INT) RETURNS TEXT AS $$
1955 DECLARE
1956     normalizer  RECORD;
1957 BEGIN
1958
1959     FOR normalizer IN
1960         SELECT  n.func AS func,
1961                 n.param_count AS param_count,
1962                 m.params AS params
1963           FROM  config.index_normalizer n
1964                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
1965           WHERE m.field = mapped_field AND m.pos < 0
1966           ORDER BY m.pos LOOP
1967
1968             EXECUTE 'SELECT ' || normalizer.func || '(' ||
1969                 quote_literal( facet_text ) ||
1970                 CASE
1971                     WHEN normalizer.param_count > 0
1972                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1973                         ELSE ''
1974                     END ||
1975                 ')' INTO facet_text;
1976
1977     END LOOP;
1978
1979     RETURN facet_text;
1980 END;
1981
1982 $$ LANGUAGE PLPGSQL;
1983
1984
1985 -- This mimics a specific part of QueryParser, turning the first part of a
1986 -- classed search (search_class) into a set of classes and possibly fields.
1987 -- search_class might look like "author" or "title|proper" or "ti|uniform"
1988 -- or "au" or "au|corporate|personal" or anything like that, where the first
1989 -- element of the list you get by separating on the "|" character is either
1990 -- a registered class (config.metabib_class) or an alias
1991 -- (config.metabib_search_alias), and the rest of any such elements are
1992 -- fields (config.metabib_field).
1993 CREATE OR REPLACE
1994     FUNCTION metabib.search_class_to_registered_components(search_class TEXT)
1995     RETURNS SETOF RECORD AS $func$
1996 DECLARE
1997     search_parts        TEXT[];
1998     field_name          TEXT;
1999     search_part_count   INTEGER;
2000     rec                 RECORD;
2001     registered_class    config.metabib_class%ROWTYPE;
2002     registered_alias    config.metabib_search_alias%ROWTYPE;
2003     registered_field    config.metabib_field%ROWTYPE;
2004 BEGIN
2005     search_parts := REGEXP_SPLIT_TO_ARRAY(search_class, E'\\|');
2006
2007     search_part_count := ARRAY_LENGTH(search_parts, 1);
2008     IF search_part_count = 0 THEN
2009         RETURN;
2010     ELSE
2011         SELECT INTO registered_class
2012             * FROM config.metabib_class WHERE name = search_parts[1];
2013         IF FOUND THEN
2014             IF search_part_count < 2 THEN   -- all fields
2015                 rec := (registered_class.name, NULL::INTEGER);
2016                 RETURN NEXT rec;
2017                 RETURN; -- done
2018             END IF;
2019             FOR field_name IN SELECT *
2020                 FROM UNNEST(search_parts[2:search_part_count]) LOOP
2021                 SELECT INTO registered_field
2022                     * FROM config.metabib_field
2023                     WHERE name = field_name AND
2024                         field_class = registered_class.name;
2025                 IF FOUND THEN
2026                     rec := (registered_class.name, registered_field.id);
2027                     RETURN NEXT rec;
2028                 END IF;
2029             END LOOP;
2030         ELSE
2031             -- maybe we have an alias?
2032             SELECT INTO registered_alias
2033                 * FROM config.metabib_search_alias WHERE alias=search_parts[1];
2034             IF NOT FOUND THEN
2035                 RETURN;
2036             ELSE
2037                 IF search_part_count < 2 THEN   -- return w/e the alias says
2038                     rec := (
2039                         registered_alias.field_class, registered_alias.field
2040                     );
2041                     RETURN NEXT rec;
2042                     RETURN; -- done
2043                 ELSE
2044                     FOR field_name IN SELECT *
2045                         FROM UNNEST(search_parts[2:search_part_count]) LOOP
2046                         SELECT INTO registered_field
2047                             * FROM config.metabib_field
2048                             WHERE name = field_name AND
2049                                 field_class = registered_alias.field_class;
2050                         IF FOUND THEN
2051                             rec := (
2052                                 registered_alias.field_class,
2053                                 registered_field.id
2054                             );
2055                             RETURN NEXT rec;
2056                         END IF;
2057                     END LOOP;
2058                 END IF;
2059             END IF;
2060         END IF;
2061     END IF;
2062 END;
2063 $func$ LANGUAGE PLPGSQL ROWS 1;
2064
2065
2066 -- Given a string such as a user might type into a search box, prepare
2067 -- two changed variants for TO_TSQUERY(). See
2068 -- http://www.postgresql.org/docs/9.0/static/textsearch-controls.html
2069 -- The first variant is normalized to match indexed documents regardless
2070 -- of diacritics.  The second variant keeps its diacritics for proper
2071 -- highlighting via TS_HEADLINE().
2072 CREATE OR REPLACE
2073     FUNCTION metabib.autosuggest_prepare_tsquery(orig TEXT) RETURNS TEXT[] AS
2074 $$
2075 DECLARE
2076     orig_ended_in_space     BOOLEAN;
2077     result                  RECORD;
2078     plain                   TEXT;
2079     normalized              TEXT;
2080 BEGIN
2081     orig_ended_in_space := orig ~ E'\\s$';
2082
2083     orig := ARRAY_TO_STRING(
2084         evergreen.regexp_split_to_array(orig, E'\\W+'), ' '
2085     );
2086
2087     normalized := public.naco_normalize(orig); -- also trim()s
2088     plain := trim(orig);
2089
2090     IF NOT orig_ended_in_space THEN
2091         plain := plain || ':*';
2092         normalized := normalized || ':*';
2093     END IF;
2094
2095     plain := ARRAY_TO_STRING(
2096         evergreen.regexp_split_to_array(plain, E'\\s+'), ' & '
2097     );
2098     normalized := ARRAY_TO_STRING(
2099         evergreen.regexp_split_to_array(normalized, E'\\s+'), ' & '
2100     );
2101
2102     RETURN ARRAY[normalized, plain];
2103 END;
2104 $$ LANGUAGE PLPGSQL;
2105
2106 -- Functions metabib.browse, metabib.staged_browse, and metabib.suggest_browse_entries
2107 -- will be created later, after internal dependencies are resolved.
2108
2109 CREATE OR REPLACE FUNCTION public.oils_tsearch2 () RETURNS TRIGGER AS $$
2110 DECLARE
2111     normalizer      RECORD;
2112     value           TEXT := '';
2113     temp_vector     TEXT := '';
2114     ts_rec          RECORD;
2115     cur_weight      "char";
2116 BEGIN
2117
2118     value := NEW.value;
2119     NEW.index_vector = ''::tsvector;
2120
2121     IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
2122         FOR normalizer IN
2123             SELECT  n.func AS func,
2124                     n.param_count AS param_count,
2125                     m.params AS params
2126               FROM  config.index_normalizer n
2127                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
2128               WHERE field = NEW.field AND m.pos < 0
2129               ORDER BY m.pos LOOP
2130                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
2131                     quote_literal( value ) ||
2132                     CASE
2133                         WHEN normalizer.param_count > 0
2134                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
2135                             ELSE ''
2136                         END ||
2137                     ')' INTO value;
2138
2139         END LOOP;
2140
2141         NEW.value = value;
2142
2143         FOR normalizer IN
2144             SELECT  n.func AS func,
2145                     n.param_count AS param_count,
2146                     m.params AS params
2147               FROM  config.index_normalizer n
2148                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
2149               WHERE field = NEW.field AND m.pos >= 0
2150               ORDER BY m.pos LOOP
2151                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
2152                     quote_literal( value ) ||
2153                     CASE
2154                         WHEN normalizer.param_count > 0
2155                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
2156                             ELSE ''
2157                         END ||
2158                     ')' INTO value;
2159
2160         END LOOP;
2161    END IF;
2162
2163     IF TG_TABLE_NAME::TEXT ~ 'browse_entry$' THEN
2164
2165         value :=  ARRAY_TO_STRING(
2166             evergreen.regexp_split_to_array(value, E'\\W+'), ' '
2167         );
2168         value := public.search_normalize(value);
2169         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
2170
2171     ELSIF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
2172         FOR ts_rec IN
2173
2174             SELECT DISTINCT m.ts_config, m.index_weight
2175             FROM config.metabib_class_ts_map m
2176                  LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
2177                  LEFT JOIN config.coded_value_map ccvm ON (
2178                     ccvm.ctype IN ('item_lang', 'language') AND
2179                     ccvm.code = m.index_lang AND
2180                     r.vlist @> intset(ccvm.id)
2181                 )
2182             WHERE m.field_class = TG_ARGV[0]
2183                 AND m.active
2184                 AND (m.always OR NOT EXISTS (SELECT 1 FROM config.metabib_field_ts_map WHERE metabib_field = NEW.field))
2185                 AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
2186                         UNION
2187             SELECT DISTINCT m.ts_config, m.index_weight
2188             FROM config.metabib_field_ts_map m
2189                  LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
2190                  LEFT JOIN config.coded_value_map ccvm ON (
2191                     ccvm.ctype IN ('item_lang', 'language') AND
2192                     ccvm.code = m.index_lang AND
2193                     r.vlist @> intset(ccvm.id)
2194                 )
2195             WHERE m.metabib_field = NEW.field
2196                 AND m.active
2197                 AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
2198             ORDER BY index_weight ASC
2199
2200         LOOP
2201
2202             IF cur_weight IS NOT NULL AND cur_weight != ts_rec.index_weight THEN
2203                 NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
2204                 temp_vector = '';
2205             END IF;
2206
2207             cur_weight = ts_rec.index_weight;
2208             SELECT INTO temp_vector temp_vector || ' ' || to_tsvector(ts_rec.ts_config::regconfig, value)::TEXT;
2209
2210         END LOOP;
2211         NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
2212     ELSE
2213         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
2214     END IF;
2215
2216     RETURN NEW;
2217 END;
2218 $$ LANGUAGE PLPGSQL;
2219
2220
2221 CREATE TYPE metabib.flat_browse_entry_appearance AS (
2222     browse_entry    BIGINT,
2223     value           TEXT,
2224     fields          TEXT,
2225     authorities     TEXT,
2226     sees            TEXT,
2227     sources         INT,        -- visible ones, that is
2228     asources        INT,        -- visible ones, that is
2229     row_number      INT,        -- internal use, sort of
2230     accurate        BOOL,       -- Count in sources field is accurate? Not
2231                                 -- if we had more than a browse superpage
2232                                 -- of records to look at.
2233     aaccurate       BOOL,       -- See previous comment...
2234     pivot_point     BIGINT
2235 );
2236
2237
2238 CREATE OR REPLACE FUNCTION metabib.browse_bib_pivot(
2239     INT[],
2240     TEXT
2241 ) RETURNS BIGINT AS $p$
2242     SELECT  mbe.id
2243       FROM  metabib.browse_entry mbe
2244             JOIN metabib.browse_entry_def_map mbedm ON (
2245                 mbedm.entry = mbe.id
2246                 AND mbedm.def = ANY($1)
2247             )
2248       WHERE mbe.sort_value >= public.naco_normalize($2)
2249       ORDER BY mbe.sort_value, mbe.value LIMIT 1;
2250 $p$ LANGUAGE SQL STABLE;
2251
2252 CREATE OR REPLACE FUNCTION metabib.browse_authority_pivot(
2253     INT[],
2254     TEXT
2255 ) RETURNS BIGINT AS $p$
2256     SELECT  mbe.id
2257       FROM  metabib.browse_entry mbe
2258             JOIN metabib.browse_entry_simple_heading_map mbeshm ON ( mbeshm.entry = mbe.id )
2259             JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2260             JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2261                 ash.atag = map.authority_field
2262                 AND map.metabib_field = ANY($1)
2263             )
2264       WHERE mbe.sort_value >= public.naco_normalize($2)
2265       ORDER BY mbe.sort_value, mbe.value LIMIT 1;
2266 $p$ LANGUAGE SQL STABLE;
2267
2268 CREATE OR REPLACE FUNCTION metabib.browse_authority_refs_pivot(
2269     INT[],
2270     TEXT
2271 ) RETURNS BIGINT AS $p$
2272     SELECT  mbe.id
2273       FROM  metabib.browse_entry mbe
2274             JOIN metabib.browse_entry_simple_heading_map mbeshm ON ( mbeshm.entry = mbe.id )
2275             JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2276             JOIN authority.control_set_auth_field_metabib_field_map_refs_only map ON (
2277                 ash.atag = map.authority_field
2278                 AND map.metabib_field = ANY($1)
2279             )
2280       WHERE mbe.sort_value >= public.naco_normalize($2)
2281       ORDER BY mbe.sort_value, mbe.value LIMIT 1;
2282 $p$ LANGUAGE SQL STABLE;
2283
2284 CREATE OR REPLACE FUNCTION metabib.browse_pivot(
2285     INT[],
2286     TEXT
2287 ) RETURNS BIGINT AS $p$
2288     SELECT  id FROM metabib.browse_entry
2289       WHERE id IN (
2290                 metabib.browse_bib_pivot($1, $2),
2291                 metabib.browse_authority_refs_pivot($1,$2) -- only look in 4xx, 5xx, 7xx of authority
2292             )
2293       ORDER BY sort_value, value LIMIT 1;
2294 $p$ LANGUAGE SQL STABLE;
2295
2296
2297 -- This function is used to help clean up facet labels. Due to quirks in
2298 -- MARC parsing, some facet labels may be generated with periods or commas
2299 -- at the end.  This will strip a trailing commas off all the time, and
2300 -- periods when they don't look like they are part of initials.
2301 --      Smith, John    =>  no change
2302 --      Smith, John,   =>  Smith, John
2303 --      Smith, John.   =>  Smith, John
2304 --      Public, John Q. => no change
2305 CREATE OR REPLACE FUNCTION metabib.trim_trailing_punctuation ( TEXT ) RETURNS TEXT AS $$
2306 DECLARE
2307     result    TEXT;
2308     last_char TEXT;
2309 BEGIN
2310     result := $1;
2311     last_char = substring(result from '.$');
2312
2313     IF last_char = ',' THEN
2314         result := substring(result from '^(.*),$');
2315
2316     ELSIF last_char = '.' THEN
2317         IF substring(result from ' \w\.$') IS NULL THEN
2318             result := substring(result from '^(.*)\.$');
2319         END IF;
2320     END IF;
2321
2322     RETURN result;
2323
2324 END;
2325 $$ language 'plpgsql';
2326
2327 COMMIT;
2328