LP#1269911: Upgrade script for MVF and CRA
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 030.schema.metabib.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2007-2008  Equinox Software, Inc.
4  * Mike Rylander <miker@esilibrary.com> 
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  */
17
18 DROP SCHEMA IF EXISTS metabib CASCADE;
19
20 BEGIN;
21 CREATE SCHEMA metabib;
22
23 CREATE TABLE metabib.metarecord (
24         id              BIGSERIAL       PRIMARY KEY,
25         fingerprint     TEXT            NOT NULL,
26         master_record   BIGINT,
27         mods            TEXT
28 );
29 CREATE INDEX metabib_metarecord_master_record_idx ON metabib.metarecord (master_record);
30 CREATE INDEX metabib_metarecord_fingerprint_idx ON metabib.metarecord (fingerprint);
31
32 CREATE TABLE metabib.identifier_field_entry (
33         id              BIGSERIAL       PRIMARY KEY,
34         source          BIGINT          NOT NULL,
35         field           INT             NOT NULL,
36         value           TEXT            NOT NULL,
37         index_vector    tsvector        NOT NULL
38 );
39 CREATE TRIGGER metabib_identifier_field_entry_fti_trigger
40         BEFORE UPDATE OR INSERT ON metabib.identifier_field_entry
41         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('identifier');
42
43 CREATE INDEX metabib_identifier_field_entry_index_vector_idx ON metabib.identifier_field_entry USING GIST (index_vector);
44 CREATE INDEX metabib_identifier_field_entry_value_idx ON metabib.identifier_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
45 CREATE INDEX metabib_identifier_field_entry_source_idx ON metabib.identifier_field_entry (source);
46
47 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 GIST (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 GIST (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 GIST (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 GIST (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 GIST (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 GIST (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 GIST (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 GIST (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 GIST (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 GIST (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 GIST (index_vector);
174 CREATE INDEX metabib_combined_series_field_source_idx ON metabib.combined_series_field_entry (metabib_field);
175
176 CREATE TABLE metabib.facet_entry (
177         id              BIGSERIAL       PRIMARY KEY,
178         source          BIGINT          NOT NULL,
179         field           INT             NOT NULL,
180         value           TEXT            NOT NULL
181 );
182 CREATE INDEX metabib_facet_entry_field_idx ON metabib.facet_entry (field);
183 CREATE INDEX metabib_facet_entry_value_idx ON metabib.facet_entry (SUBSTRING(value,1,1024));
184 CREATE INDEX metabib_facet_entry_source_idx ON metabib.facet_entry (source);
185
186 CREATE TABLE metabib.browse_entry (
187     id BIGSERIAL PRIMARY KEY,
188     value TEXT,
189     index_vector tsvector,
190     sort_value  TEXT NOT NULL,
191     UNIQUE(sort_value, value)
192 );
193
194
195 CREATE INDEX browse_entry_sort_value_idx
196     ON metabib.browse_entry USING BTREE (sort_value);
197
198 CREATE INDEX metabib_browse_entry_index_vector_idx ON metabib.browse_entry USING GIN (index_vector);
199 CREATE TRIGGER metabib_browse_entry_fti_trigger
200     BEFORE INSERT OR UPDATE ON metabib.browse_entry
201     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
202
203
204 CREATE TABLE metabib.browse_entry_def_map (
205     id BIGSERIAL PRIMARY KEY,
206     entry BIGINT REFERENCES metabib.browse_entry (id),
207     def INT REFERENCES config.metabib_field (id) ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
208     source BIGINT REFERENCES biblio.record_entry (id),
209     authority BIGINT REFERENCES authority.record_entry (id) ON DELETE SET NULL
210 );
211 CREATE INDEX browse_entry_def_map_def_idx ON metabib.browse_entry_def_map (def);
212 CREATE INDEX browse_entry_def_map_entry_idx ON metabib.browse_entry_def_map (entry);
213 CREATE INDEX browse_entry_def_map_source_idx ON metabib.browse_entry_def_map (source);
214
215 CREATE TABLE metabib.browse_entry_simple_heading_map (
216     id BIGSERIAL PRIMARY KEY,
217     entry BIGINT REFERENCES metabib.browse_entry (id),
218     simple_heading BIGINT REFERENCES authority.simple_heading (id) ON DELETE CASCADE
219 );
220 CREATE INDEX browse_entry_sh_map_entry_idx ON metabib.browse_entry_simple_heading_map (entry);
221 CREATE INDEX browse_entry_sh_map_sh_idx ON metabib.browse_entry_simple_heading_map (simple_heading);
222
223 CREATE OR REPLACE FUNCTION metabib.facet_normalize_trigger () RETURNS TRIGGER AS $$
224 DECLARE
225     normalizer  RECORD;
226     facet_text  TEXT;
227 BEGIN
228     facet_text := NEW.value;
229
230     FOR normalizer IN
231         SELECT  n.func AS func,
232                 n.param_count AS param_count,
233                 m.params AS params
234           FROM  config.index_normalizer n
235                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
236           WHERE m.field = NEW.field AND m.pos < 0
237           ORDER BY m.pos LOOP
238
239             EXECUTE 'SELECT ' || normalizer.func || '(' ||
240                 quote_literal( facet_text ) ||
241                 CASE
242                     WHEN normalizer.param_count > 0
243                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
244                         ELSE ''
245                     END ||
246                 ')' INTO facet_text;
247
248     END LOOP;
249
250     NEW.value = facet_text;
251
252     RETURN NEW;
253 END;
254 $$ LANGUAGE PLPGSQL;
255
256 CREATE TRIGGER facet_normalize_tgr
257         BEFORE UPDATE OR INSERT ON metabib.facet_entry
258         FOR EACH ROW EXECUTE PROCEDURE metabib.facet_normalize_trigger();
259
260 CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
261 BEGIN
262     NEW.value := force_unicode_normal_form(NEW.value,'NFC');
263     RETURN NEW;
264 END;
265 $$ LANGUAGE PLPGSQL;
266
267 CREATE TRIGGER facet_force_nfc_tgr
268         BEFORE UPDATE OR INSERT ON metabib.facet_entry
269         FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
270
271 -- DECREMENTING serial starts at -1
272 CREATE SEQUENCE metabib.uncontrolled_record_attr_value_id_seq INCREMENT BY -1;
273
274 CREATE TABLE metabib.uncontrolled_record_attr_value (
275     id      BIGINT  PRIMARY KEY DEFAULT nextval('metabib.uncontrolled_record_attr_value_id_seq'),
276     attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name),
277     value   TEXT    NOT NULL
278 );
279 CREATE UNIQUE INDEX muv_once_idx ON metabib.uncontrolled_record_attr_value (attr,value);
280
281 CREATE VIEW metabib.record_attr_id_map AS
282     SELECT id, attr, value FROM metabib.uncontrolled_record_attr_value
283         UNION
284     SELECT  c.id, c.ctype AS attr, c.code AS value
285       FROM  config.coded_value_map c
286             JOIN config.record_attr_definition d ON (d.name = c.ctype AND NOT d.composite);
287
288 CREATE VIEW metabib.composite_attr_id_map AS
289     SELECT  c.id, c.ctype AS attr, c.code AS value
290       FROM  config.coded_value_map c
291             JOIN config.record_attr_definition d ON (d.name = c.ctype AND d.composite);
292
293 CREATE VIEW metabib.full_attr_id_map AS
294     SELECT id, attr, value FROM metabib.record_attr_id_map
295         UNION
296     SELECT id, attr, value FROM metabib.composite_attr_id_map;
297
298
299 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_def TEXT ) RETURNS query_int AS $func$
300
301     use JSON::XS;
302     my $def = decode_json(shift);
303
304     die("Composite attribute definition not supplied") unless $def;
305
306     sub recurse {
307         my $d = shift;
308         my $j = '&';
309         my @list;
310
311         if (ref $d eq 'HASH') { # node or AND
312             if (exists $d->{_attr}) { # it is a node
313                 my $plan = spi_prepare('SELECT * FROM metabib.full_attr_id_map WHERE attr = $1 AND value = $2', qw/TEXT TEXT/);
314                 return spi_exec_prepared(
315                     $plan, {limit => 1}, $d->{_attr}, $d->{_val}
316                 )->{rows}[0]{id};
317                 spi_freeplan($plan);
318             } elsif (exists $d->{_not} && scalar(keys(%$d)) == 1) { # it is a NOT
319                 return '!' . recurse($$d{_not});
320             } else { # an AND list
321                 @list = map { recurse($$d{$_}) } sort keys %$d;
322             }
323         } elsif (ref $d eq 'ARRAY') {
324             $j = '|';
325             @list = map { recurse($_) } @$d;
326         }
327
328         @list = grep { defined && $_ ne '' } @list;
329
330         return '(' . join($j,@list) . ')' if @list;
331         return '';
332     }
333
334     return recurse($def) || undef;
335
336 $func$ IMMUTABLE LANGUAGE plperlu;
337
338 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr ( cattr_id INT ) RETURNS query_int AS $func$
339     SELECT metabib.compile_composite_attr(definition) FROM config.composite_attr_entry_definition WHERE coded_value = $1;
340 $func$ STRICT IMMUTABLE LANGUAGE SQL;
341
342 CREATE TABLE metabib.record_attr_vector_list (
343     source  BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id),
344     vlist   INT[]   NOT NULL -- stores id from ccvm AND murav
345 );
346 CREATE INDEX mrca_vlist_idx ON metabib.record_attr_vector_list USING gin ( vlist gin__int_ops );
347
348 /* This becomes a view, and we do sorters differently ...
349 CREATE TABLE metabib.record_attr (
350         id              BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
351         attrs   HSTORE  NOT NULL DEFAULT ''::HSTORE
352 );
353 CREATE INDEX metabib_svf_attrs_idx ON metabib.record_attr USING GIST (attrs);
354 CREATE INDEX metabib_svf_date1_idx ON metabib.record_attr ((attrs->'date1'));
355 CREATE INDEX metabib_svf_dates_idx ON metabib.record_attr ((attrs->'date1'),(attrs->'date2'));
356 */
357
358 /* ... like this */
359 CREATE TABLE metabib.record_sorter (
360     id      BIGSERIAL   PRIMARY KEY,
361     source  BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
362     attr    TEXT        NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE,
363     value   TEXT        NOT NULL
364 );
365 CREATE INDEX metabib_sorter_source_idx ON metabib.record_sorter (source); -- we may not need one of this or the next ... stats will tell
366 CREATE INDEX metabib_sorter_s_a_idx ON metabib.record_sorter (source, attr);
367 CREATE INDEX metabib_sorter_a_v_idx ON metabib.record_sorter (attr, value);
368
369
370 CREATE TYPE metabib.record_attr_type AS (
371     id      BIGINT,
372     attrs   HSTORE
373 );
374
375 -- Back-compat view ... we're moving to an INTARRAY world
376 CREATE VIEW metabib.record_attr_flat AS
377     SELECT  v.source AS id,
378             m.attr,
379             m.value
380       FROM  metabib.full_attr_id_map m
381             JOIN  metabib.record_attr_vector_list v ON ( m.id = ANY( v.vlist ) );
382
383 CREATE VIEW metabib.record_attr AS
384     SELECT id, HSTORE( ARRAY_AGG( attr ), ARRAY_AGG( value ) ) AS attrs FROM metabib.record_attr_flat GROUP BY 1;
385
386 -- Back-back-compat view ... we use to live in an HSTORE world
387 CREATE TYPE metabib.rec_desc_type AS (
388     item_type       TEXT,
389     item_form       TEXT,
390     bib_level       TEXT,
391     control_type    TEXT,
392     char_encoding   TEXT,
393     enc_level       TEXT,
394     audience        TEXT,
395     lit_form        TEXT,
396     type_mat        TEXT,
397     cat_form        TEXT,
398     pub_status      TEXT,
399     item_lang       TEXT,
400     vr_format       TEXT,
401     date1           TEXT,
402     date2           TEXT
403 );
404
405 CREATE VIEW metabib.rec_descriptor AS
406     SELECT  id,
407             id AS record,
408             (populate_record(NULL::metabib.rec_desc_type, attrs)).*
409       FROM  metabib.record_attr;
410
411 -- Use a sequence that matches previous version, for easier upgrading.
412 CREATE SEQUENCE metabib.full_rec_id_seq;
413
414 CREATE TABLE metabib.real_full_rec (
415         id                  BIGINT      NOT NULL DEFAULT NEXTVAL('metabib.full_rec_id_seq'::REGCLASS),
416         record          BIGINT          NOT NULL,
417         tag             CHAR(3)         NOT NULL,
418         ind1            TEXT,
419         ind2            TEXT,
420         subfield        TEXT,
421         value           TEXT            NOT NULL,
422         index_vector    tsvector        NOT NULL
423 );
424 ALTER TABLE metabib.real_full_rec ADD PRIMARY KEY (id);
425
426 CREATE INDEX metabib_full_rec_tag_subfield_idx ON metabib.real_full_rec (tag,subfield);
427 CREATE INDEX metabib_full_rec_value_idx ON metabib.real_full_rec (substring(value,1,1024));
428 /* Enable LIKE to use an index for database clusters with locales other than C or POSIX */
429 CREATE INDEX metabib_full_rec_value_tpo_index ON metabib.real_full_rec (substring(value,1,1024) text_pattern_ops);
430 CREATE INDEX metabib_full_rec_record_idx ON metabib.real_full_rec (record);
431 CREATE INDEX metabib_full_rec_index_vector_idx ON metabib.real_full_rec USING GIST (index_vector);
432 CREATE INDEX metabib_full_rec_isxn_caseless_idx
433     ON metabib.real_full_rec (LOWER(value))
434     WHERE tag IN ('020', '022', '024');
435 -- This next index might fully supplant the one above, but leaving both for now.
436 -- (they are not too large)
437 -- The reason we need this index is to ensure that the query parser always
438 -- prefers this index over the simpler tag/subfield index, as this greatly
439 -- increases Vandelay overlay speed for these identifiers, especially when
440 -- a record has many of these fields (around > 4-6 seems like the cutoff
441 -- on at least one PG9.1 system)
442 -- A similar index could be added for other fields (e.g. 010), but one should
443 -- leave out the LOWER() in all other cases.
444 -- TODO: verify whether we can discard the non tag/subfield/substring version
445 -- above (metabib_full_rec_isxn_caseless_idx)
446 CREATE INDEX metabib_full_rec_02x_tag_subfield_lower_substring
447     ON metabib.real_full_rec (tag, subfield, LOWER(substring(value, 1, 1024)))
448     WHERE tag IN ('020', '022', '024');
449
450
451 CREATE TRIGGER metabib_full_rec_fti_trigger
452         BEFORE UPDATE OR INSERT ON metabib.real_full_rec
453         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('default');
454
455 CREATE OR REPLACE VIEW metabib.full_rec AS
456     SELECT  id,
457             record,
458             tag,
459             ind1,
460             ind2,
461             subfield,
462             SUBSTRING(value,1,1024) AS value,
463             index_vector
464       FROM  metabib.real_full_rec;
465
466 CREATE OR REPLACE RULE metabib_full_rec_insert_rule
467     AS ON INSERT TO metabib.full_rec
468     DO INSTEAD
469     INSERT INTO metabib.real_full_rec VALUES (
470         COALESCE(NEW.id, NEXTVAL('metabib.full_rec_id_seq'::REGCLASS)),
471         NEW.record,
472         NEW.tag,
473         NEW.ind1,
474         NEW.ind2,
475         NEW.subfield,
476         NEW.value,
477         NEW.index_vector
478     );
479
480 CREATE OR REPLACE RULE metabib_full_rec_update_rule
481     AS ON UPDATE TO metabib.full_rec
482     DO INSTEAD
483     UPDATE  metabib.real_full_rec SET
484         id = NEW.id,
485         record = NEW.record,
486         tag = NEW.tag,
487         ind1 = NEW.ind1,
488         ind2 = NEW.ind2,
489         subfield = NEW.subfield,
490         value = NEW.value,
491         index_vector = NEW.index_vector
492       WHERE id = OLD.id;
493
494 CREATE OR REPLACE RULE metabib_full_rec_delete_rule
495     AS ON DELETE TO metabib.full_rec
496     DO INSTEAD
497     DELETE FROM metabib.real_full_rec WHERE id = OLD.id;
498
499 CREATE TABLE metabib.metarecord_source_map (
500         id              BIGSERIAL       PRIMARY KEY,
501         metarecord      BIGINT          NOT NULL,
502         source          BIGINT          NOT NULL
503 );
504 CREATE INDEX metabib_metarecord_source_map_metarecord_idx ON metabib.metarecord_source_map (metarecord);
505 CREATE INDEX metabib_metarecord_source_map_source_record_idx ON metabib.metarecord_source_map (source);
506
507 CREATE TYPE metabib.field_entry_template AS (
508     field_class         TEXT,
509     field               INT,
510     facet_field         BOOL,
511     search_field        BOOL,
512     browse_field        BOOL,
513     source              BIGINT,
514     value               TEXT,
515     authority           BIGINT,
516     sort_value          TEXT
517 );
518
519
520 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( rid BIGINT, default_joiner TEXT ) RETURNS SETOF metabib.field_entry_template AS $func$
521 DECLARE
522     bib     biblio.record_entry%ROWTYPE;
523     idx     config.metabib_field%ROWTYPE;
524     xfrm        config.xml_transform%ROWTYPE;
525     prev_xfrm   TEXT;
526     transformed_xml TEXT;
527     xml_node    TEXT;
528     xml_node_list   TEXT[];
529     facet_text  TEXT;
530     browse_text TEXT;
531     sort_value  TEXT;
532     raw_text    TEXT;
533     curr_text   TEXT;
534     joiner      TEXT := default_joiner; -- XXX will index defs supply a joiner?
535     authority_text TEXT;
536     authority_link BIGINT;
537     output_row  metabib.field_entry_template%ROWTYPE;
538 BEGIN
539
540     -- Start out with no field-use bools set
541     output_row.browse_field = FALSE;
542     output_row.facet_field = FALSE;
543     output_row.search_field = FALSE;
544
545     -- Get the record
546     SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
547
548     -- Loop over the indexing entries
549     FOR idx IN SELECT * FROM config.metabib_field ORDER BY format LOOP
550
551         joiner := COALESCE(idx.joiner, default_joiner);
552
553         SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
554
555         -- See if we can skip the XSLT ... it's expensive
556         IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
557             -- Can't skip the transform
558             IF xfrm.xslt <> '---' THEN
559                 transformed_xml := oils_xslt_process(bib.marc,xfrm.xslt);
560             ELSE
561                 transformed_xml := bib.marc;
562             END IF;
563
564             prev_xfrm := xfrm.name;
565         END IF;
566
567         xml_node_list := oils_xpath( idx.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
568
569         raw_text := NULL;
570         FOR xml_node IN SELECT x FROM unnest(xml_node_list) AS x LOOP
571             CONTINUE WHEN xml_node !~ E'^\\s*<';
572
573             -- XXX much of this should be moved into oils_xpath_string...
574             curr_text := ARRAY_TO_STRING(evergreen.array_remove_item_by_value(evergreen.array_remove_item_by_value(
575                 oils_xpath( '//text()',
576                     REGEXP_REPLACE(
577                         REGEXP_REPLACE( -- This escapes all &s not followed by "amp;".  Data ise returned from oils_xpath (above) in UTF-8, not entity encoded
578                             REGEXP_REPLACE( -- This escapes embeded <s
579                                 xml_node,
580                                 $re$(>[^<]+)(<)([^>]+<)$re$,
581                                 E'\\1&lt;\\3',
582                                 'g'
583                             ),
584                             '&(?!amp;)',
585                             '&amp;',
586                             'g'
587                         ),
588                         E'\\s+',
589                         ' ',
590                         'g'
591                     )
592                 ), ' '), ''),
593                 joiner
594             );
595
596             CONTINUE WHEN curr_text IS NULL OR curr_text = '';
597
598             IF raw_text IS NOT NULL THEN
599                 raw_text := raw_text || joiner;
600             END IF;
601
602             raw_text := COALESCE(raw_text,'') || curr_text;
603
604             -- autosuggest/metabib.browse_entry
605             IF idx.browse_field THEN
606
607                 IF idx.browse_xpath IS NOT NULL AND idx.browse_xpath <> '' THEN
608                     browse_text := oils_xpath_string( idx.browse_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
609                 ELSE
610                     browse_text := curr_text;
611                 END IF;
612
613                 IF idx.browse_sort_xpath IS NOT NULL AND
614                     idx.browse_sort_xpath <> '' THEN
615
616                     sort_value := oils_xpath_string(
617                         idx.browse_sort_xpath, xml_node, joiner,
618                         ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
619                     );
620                 ELSE
621                     sort_value := browse_text;
622                 END IF;
623
624                 output_row.field_class = idx.field_class;
625                 output_row.field = idx.id;
626                 output_row.source = rid;
627                 output_row.value = BTRIM(REGEXP_REPLACE(browse_text, E'\\s+', ' ', 'g'));
628                 output_row.sort_value :=
629                     public.naco_normalize(sort_value);
630
631                 output_row.authority := NULL;
632
633                 IF idx.authority_xpath IS NOT NULL AND idx.authority_xpath <> '' THEN
634                     authority_text := oils_xpath_string(
635                         idx.authority_xpath, xml_node, joiner,
636                         ARRAY[
637                             ARRAY[xfrm.prefix, xfrm.namespace_uri],
638                             ARRAY['xlink','http://www.w3.org/1999/xlink']
639                         ]
640                     );
641
642                     IF authority_text ~ '^\d+$' THEN
643                         authority_link := authority_text::BIGINT;
644                         PERFORM * FROM authority.record_entry WHERE id = authority_link;
645                         IF FOUND THEN
646                             output_row.authority := authority_link;
647                         END IF;
648                     END IF;
649
650                 END IF;
651
652                 output_row.browse_field = TRUE;
653                 -- Returning browse rows with search_field = true for search+browse
654                 -- configs allows us to retain granularity of being able to search
655                 -- browse fields with "starts with" type operators (for example, for
656                 -- titles of songs in music albums)
657                 IF idx.search_field THEN
658                     output_row.search_field = TRUE;
659                 END IF;
660                 RETURN NEXT output_row;
661                 output_row.browse_field = FALSE;
662                 output_row.search_field = FALSE;
663                 output_row.sort_value := NULL;
664             END IF;
665
666             -- insert raw node text for faceting
667             IF idx.facet_field THEN
668
669                 IF idx.facet_xpath IS NOT NULL AND idx.facet_xpath <> '' THEN
670                     facet_text := oils_xpath_string( idx.facet_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
671                 ELSE
672                     facet_text := curr_text;
673                 END IF;
674
675                 output_row.field_class = idx.field_class;
676                 output_row.field = -1 * idx.id;
677                 output_row.source = rid;
678                 output_row.value = BTRIM(REGEXP_REPLACE(facet_text, E'\\s+', ' ', 'g'));
679
680                 output_row.facet_field = TRUE;
681                 RETURN NEXT output_row;
682                 output_row.facet_field = FALSE;
683             END IF;
684
685         END LOOP;
686
687         CONTINUE WHEN raw_text IS NULL OR raw_text = '';
688
689         -- insert combined node text for searching
690         IF idx.search_field THEN
691             output_row.field_class = idx.field_class;
692             output_row.field = idx.id;
693             output_row.source = rid;
694             output_row.value = BTRIM(REGEXP_REPLACE(raw_text, E'\\s+', ' ', 'g'));
695
696             output_row.search_field = TRUE;
697             RETURN NEXT output_row;
698             output_row.search_field = FALSE;
699         END IF;
700
701     END LOOP;
702
703 END;
704
705 $func$ LANGUAGE PLPGSQL;
706
707 CREATE OR REPLACE FUNCTION metabib.update_combined_index_vectors(bib_id BIGINT) RETURNS VOID AS $func$
708 BEGIN
709     DELETE FROM metabib.combined_keyword_field_entry WHERE record = bib_id;
710     INSERT INTO metabib.combined_keyword_field_entry(record, metabib_field, index_vector)
711         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
712         FROM metabib.keyword_field_entry WHERE source = bib_id GROUP BY field;
713     INSERT INTO metabib.combined_keyword_field_entry(record, metabib_field, index_vector)
714         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
715         FROM metabib.keyword_field_entry WHERE source = bib_id;
716
717     DELETE FROM metabib.combined_title_field_entry WHERE record = bib_id;
718     INSERT INTO metabib.combined_title_field_entry(record, metabib_field, index_vector)
719         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
720         FROM metabib.title_field_entry WHERE source = bib_id GROUP BY field;
721     INSERT INTO metabib.combined_title_field_entry(record, metabib_field, index_vector)
722         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
723         FROM metabib.title_field_entry WHERE source = bib_id;
724
725     DELETE FROM metabib.combined_author_field_entry WHERE record = bib_id;
726     INSERT INTO metabib.combined_author_field_entry(record, metabib_field, index_vector)
727         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
728         FROM metabib.author_field_entry WHERE source = bib_id GROUP BY field;
729     INSERT INTO metabib.combined_author_field_entry(record, metabib_field, index_vector)
730         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
731         FROM metabib.author_field_entry WHERE source = bib_id;
732
733     DELETE FROM metabib.combined_subject_field_entry WHERE record = bib_id;
734     INSERT INTO metabib.combined_subject_field_entry(record, metabib_field, index_vector)
735         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
736         FROM metabib.subject_field_entry WHERE source = bib_id GROUP BY field;
737     INSERT INTO metabib.combined_subject_field_entry(record, metabib_field, index_vector)
738         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
739         FROM metabib.subject_field_entry WHERE source = bib_id;
740
741     DELETE FROM metabib.combined_series_field_entry WHERE record = bib_id;
742     INSERT INTO metabib.combined_series_field_entry(record, metabib_field, index_vector)
743         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
744         FROM metabib.series_field_entry WHERE source = bib_id GROUP BY field;
745     INSERT INTO metabib.combined_series_field_entry(record, metabib_field, index_vector)
746         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
747         FROM metabib.series_field_entry WHERE source = bib_id;
748
749     DELETE FROM metabib.combined_identifier_field_entry WHERE record = bib_id;
750     INSERT INTO metabib.combined_identifier_field_entry(record, metabib_field, index_vector)
751         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
752         FROM metabib.identifier_field_entry WHERE source = bib_id GROUP BY field;
753     INSERT INTO metabib.combined_identifier_field_entry(record, metabib_field, index_vector)
754         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
755         FROM metabib.identifier_field_entry WHERE source = bib_id;
756
757 END;
758 $func$ LANGUAGE PLPGSQL;
759
760 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_field_entries( bib_id BIGINT, skip_facet BOOL DEFAULT FALSE, skip_browse BOOL DEFAULT FALSE, skip_search BOOL DEFAULT FALSE ) RETURNS VOID AS $func$
761 DECLARE
762     fclass          RECORD;
763     ind_data        metabib.field_entry_template%ROWTYPE;
764     mbe_row         metabib.browse_entry%ROWTYPE;
765     mbe_id          BIGINT;
766     b_skip_facet    BOOL;
767     b_skip_browse   BOOL;
768     b_skip_search   BOOL;
769     value_prepped   TEXT;
770 BEGIN
771
772     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;
773     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;
774     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;
775
776     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
777     IF NOT FOUND THEN
778         IF NOT b_skip_search THEN
779             FOR fclass IN SELECT * FROM config.metabib_class LOOP
780                 -- RAISE NOTICE 'Emptying out %', fclass.name;
781                 EXECUTE $$DELETE FROM metabib.$$ || fclass.name || $$_field_entry WHERE source = $$ || bib_id;
782             END LOOP;
783         END IF;
784         IF NOT b_skip_facet THEN
785             DELETE FROM metabib.facet_entry WHERE source = bib_id;
786         END IF;
787         IF NOT b_skip_browse THEN
788             DELETE FROM metabib.browse_entry_def_map WHERE source = bib_id;
789         END IF;
790     END IF;
791
792     FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( bib_id ) LOOP
793         IF ind_data.field < 0 THEN
794             ind_data.field = -1 * ind_data.field;
795         END IF;
796
797         IF ind_data.facet_field AND NOT b_skip_facet THEN
798             INSERT INTO metabib.facet_entry (field, source, value)
799                 VALUES (ind_data.field, ind_data.source, ind_data.value);
800         END IF;
801
802         IF ind_data.browse_field AND NOT b_skip_browse THEN
803             -- A caveat about this SELECT: this should take care of replacing
804             -- old mbe rows when data changes, but not if normalization (by
805             -- which I mean specifically the output of
806             -- evergreen.oils_tsearch2()) changes.  It may or may not be
807             -- expensive to add a comparison of index_vector to index_vector
808             -- to the WHERE clause below.
809
810             value_prepped := metabib.browse_normalize(ind_data.value, ind_data.field);
811             SELECT INTO mbe_row * FROM metabib.browse_entry
812                 WHERE value = value_prepped AND sort_value = ind_data.sort_value;
813
814             IF FOUND THEN
815                 mbe_id := mbe_row.id;
816             ELSE
817                 INSERT INTO metabib.browse_entry
818                     ( value, sort_value ) VALUES
819                     ( value_prepped, ind_data.sort_value );
820
821                 mbe_id := CURRVAL('metabib.browse_entry_id_seq'::REGCLASS);
822             END IF;
823
824             INSERT INTO metabib.browse_entry_def_map (entry, def, source, authority)
825                 VALUES (mbe_id, ind_data.field, ind_data.source, ind_data.authority);
826         END IF;
827
828         IF ind_data.search_field AND NOT b_skip_search THEN
829             -- Avoid inserting duplicate rows
830             EXECUTE 'SELECT 1 FROM metabib.' || ind_data.field_class ||
831                 '_field_entry WHERE field = $1 AND source = $2 AND value = $3'
832                 INTO mbe_id USING ind_data.field, ind_data.source, ind_data.value;
833                 -- RAISE NOTICE 'Search for an already matching row returned %', mbe_id;
834             IF mbe_id IS NULL THEN
835                 EXECUTE $$
836                 INSERT INTO metabib.$$ || ind_data.field_class || $$_field_entry (field, source, value)
837                     VALUES ($$ ||
838                         quote_literal(ind_data.field) || $$, $$ ||
839                         quote_literal(ind_data.source) || $$, $$ ||
840                         quote_literal(ind_data.value) ||
841                     $$);$$;
842             END IF;
843         END IF;
844
845     END LOOP;
846
847     IF NOT b_skip_search THEN
848         PERFORM metabib.update_combined_index_vectors(bib_id);
849     END IF;
850
851     RETURN;
852 END;
853 $func$ LANGUAGE PLPGSQL;
854
855 -- default to a space joiner
856 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( BIGINT ) RETURNS SETOF metabib.field_entry_template AS $func$
857         SELECT * FROM biblio.extract_metabib_field_entry($1, ' ');
858 $func$ LANGUAGE SQL;
859
860 CREATE OR REPLACE FUNCTION authority.flatten_marc ( rid BIGINT ) RETURNS SETOF authority.full_rec AS $func$
861 DECLARE
862         auth    authority.record_entry%ROWTYPE;
863         output  authority.full_rec%ROWTYPE;
864         field   RECORD;
865 BEGIN
866         SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
867
868         FOR field IN SELECT * FROM vandelay.flatten_marc( auth.marc ) LOOP
869                 output.record := rid;
870                 output.ind1 := field.ind1;
871                 output.ind2 := field.ind2;
872                 output.tag := field.tag;
873                 output.subfield := field.subfield;
874                 output.value := field.value;
875
876                 RETURN NEXT output;
877         END LOOP;
878 END;
879 $func$ LANGUAGE PLPGSQL;
880
881 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
882 DECLARE
883         bib     biblio.record_entry%ROWTYPE;
884         output  metabib.full_rec%ROWTYPE;
885         field   RECORD;
886 BEGIN
887         SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
888
889         FOR field IN SELECT * FROM vandelay.flatten_marc( bib.marc ) LOOP
890                 output.record := rid;
891                 output.ind1 := field.ind1;
892                 output.ind2 := field.ind2;
893                 output.tag := field.tag;
894                 output.subfield := field.subfield;
895                 output.value := field.value;
896
897                 RETURN NEXT output;
898         END LOOP;
899 END;
900 $func$ LANGUAGE PLPGSQL;
901
902 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
903 DECLARE
904         ldr         TEXT;
905         tval        TEXT;
906         tval_rec    RECORD;
907         bval        TEXT;
908         bval_rec    RECORD;
909     retval      config.marc21_rec_type_map%ROWTYPE;
910 BEGIN
911     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
912
913     IF ldr IS NULL OR ldr = '' THEN
914         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
915         RETURN retval;
916     END IF;
917
918     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
919     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
920
921
922     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
923     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
924
925     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
926
927     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
928
929
930     IF retval.code IS NULL THEN
931         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
932     END IF;
933
934     RETURN retval;
935 END;
936 $func$ LANGUAGE PLPGSQL;
937
938 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
939     SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
940 $func$ LANGUAGE SQL;
941
942 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field_list( marc TEXT, ff TEXT ) RETURNS TEXT[] AS $func$
943 DECLARE
944     rtype       TEXT;
945     ff_pos      RECORD;
946     tag_data    RECORD;
947     val         TEXT;
948     collection  TEXT[] := '{}'::TEXT[];
949 BEGIN
950     rtype := (vandelay.marc21_record_type( marc )).code;
951     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
952         IF ff_pos.tag = 'ldr' THEN
953             val := oils_xpath_string('//*[local-name()="leader"]', marc);
954             IF val IS NOT NULL THEN
955                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
956                 collection := collection || val;
957             END IF;
958         ELSE
959             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
960                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
961                 collection := collection || val;
962             END LOOP;
963         END IF;
964         val := REPEAT( ff_pos.default_val, ff_pos.length );
965         collection := collection || val;
966     END LOOP;
967
968     RETURN collection;
969 END;
970 $func$ LANGUAGE PLPGSQL;
971
972 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
973 DECLARE
974     rtype       TEXT;
975     ff_pos      RECORD;
976     tag_data    RECORD;
977     val         TEXT;
978 BEGIN
979     rtype := (vandelay.marc21_record_type( marc )).code;
980     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
981         IF ff_pos.tag = 'ldr' THEN
982             val := oils_xpath_string('//*[local-name()="leader"]', marc);
983             IF val IS NOT NULL THEN
984                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
985                 RETURN val;
986             END IF;
987         ELSE
988             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
989                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
990                 RETURN val;
991             END LOOP;
992         END IF;
993         val := REPEAT( ff_pos.default_val, ff_pos.length );
994         RETURN val;
995     END LOOP;
996
997     RETURN NULL;
998 END;
999 $func$ LANGUAGE PLPGSQL;
1000
1001 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field_list( rid BIGINT, ff TEXT ) RETURNS TEXT[] AS $func$
1002     SELECT * FROM vandelay.marc21_extract_fixed_field_list( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
1003 $func$ LANGUAGE SQL;
1004
1005 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
1006     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
1007 $func$ LANGUAGE SQL;
1008
1009 -- CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
1010 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
1011 DECLARE
1012     tag_data    TEXT;
1013     rtype       TEXT;
1014     ff_pos      RECORD;
1015     output      biblio.record_ff_map%ROWTYPE;
1016 BEGIN
1017     rtype := (vandelay.marc21_record_type( marc )).code;
1018
1019     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
1020         output.ff_name  := ff_pos.fixed_field;
1021         output.ff_value := NULL;
1022
1023         IF ff_pos.tag = 'ldr' THEN
1024             output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
1025             IF output.ff_value IS NOT NULL THEN
1026                 output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
1027                 RETURN NEXT output;
1028                 output.ff_value := NULL;
1029             END IF;
1030         ELSE
1031             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1032                 output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
1033                 IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
1034                 RETURN NEXT output;
1035                 output.ff_value := NULL;
1036             END LOOP;
1037         END IF;
1038
1039     END LOOP;
1040
1041     RETURN;
1042 END;
1043 $func$ LANGUAGE PLPGSQL;
1044
1045 CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
1046     SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1047 $func$ LANGUAGE SQL;
1048
1049 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1050     SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1051 $func$ LANGUAGE SQL;
1052
1053 CREATE OR REPLACE FUNCTION biblio.extract_quality ( marc TEXT, best_lang TEXT, best_type TEXT ) RETURNS INT AS $func$
1054 DECLARE
1055     qual        INT;
1056     ldr         TEXT;
1057     tval        TEXT;
1058     tval_rec    RECORD;
1059     bval        TEXT;
1060     bval_rec    RECORD;
1061     type_map    RECORD;
1062     ff_pos      RECORD;
1063     ff_tag_data TEXT;
1064 BEGIN
1065
1066     IF marc IS NULL OR marc = '' THEN
1067         RETURN NULL;
1068     END IF;
1069
1070     -- First, the count of tags
1071     qual := ARRAY_UPPER(oils_xpath('*[local-name()="datafield"]', marc), 1);
1072
1073     -- now go through a bunch of pain to get the record type
1074     IF best_type IS NOT NULL THEN
1075         ldr := (oils_xpath('//*[local-name()="leader"]/text()', marc))[1];
1076
1077         IF ldr IS NOT NULL THEN
1078             SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
1079             SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
1080
1081
1082             tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1083             bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1084
1085             -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1086
1087             SELECT * INTO type_map FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1088
1089             IF type_map.code IS NOT NULL THEN
1090                 IF best_type = type_map.code THEN
1091                     qual := qual + qual / 2;
1092                 END IF;
1093
1094                 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
1095                     ff_tag_data := SUBSTRING((oils_xpath('//*[@tag="' || ff_pos.tag || '"]/text()',marc))[1], ff_pos.start_pos + 1, ff_pos.length);
1096                     IF ff_tag_data = best_lang THEN
1097                             qual := qual + 100;
1098                     END IF;
1099                 END LOOP;
1100             END IF;
1101         END IF;
1102     END IF;
1103
1104     -- Now look for some quality metrics
1105     -- DCL record?
1106     IF ARRAY_UPPER(oils_xpath('//*[@tag="040"]/*[@code="a" and contains(.,"DLC")]', marc), 1) = 1 THEN
1107         qual := qual + 10;
1108     END IF;
1109
1110     -- From OCLC?
1111     IF (oils_xpath('//*[@tag="003"]/text()', marc))[1] ~* E'oclo?c' THEN
1112         qual := qual + 10;
1113     END IF;
1114
1115     RETURN qual;
1116
1117 END;
1118 $func$ LANGUAGE PLPGSQL;
1119
1120 CREATE OR REPLACE FUNCTION biblio.extract_fingerprint ( marc text ) RETURNS TEXT AS $func$
1121 DECLARE
1122         idx             config.biblio_fingerprint%ROWTYPE;
1123         xfrm            config.xml_transform%ROWTYPE;
1124         prev_xfrm       TEXT;
1125         transformed_xml TEXT;
1126         xml_node        TEXT;
1127         xml_node_list   TEXT[];
1128         raw_text        TEXT;
1129     output_text TEXT := '';
1130 BEGIN
1131
1132     IF marc IS NULL OR marc = '' THEN
1133         RETURN NULL;
1134     END IF;
1135
1136         -- Loop over the indexing entries
1137         FOR idx IN SELECT * FROM config.biblio_fingerprint ORDER BY format, id LOOP
1138
1139                 SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
1140
1141                 -- See if we can skip the XSLT ... it's expensive
1142                 IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1143                         -- Can't skip the transform
1144                         IF xfrm.xslt <> '---' THEN
1145                                 transformed_xml := oils_xslt_process(marc,xfrm.xslt);
1146                         ELSE
1147                                 transformed_xml := marc;
1148                         END IF;
1149
1150                         prev_xfrm := xfrm.name;
1151                 END IF;
1152
1153                 raw_text := COALESCE(
1154             naco_normalize(
1155                 ARRAY_TO_STRING(
1156                     oils_xpath(
1157                         '//text()',
1158                         (oils_xpath(
1159                             idx.xpath,
1160                             transformed_xml,
1161                             ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] 
1162                         ))[1]
1163                     ),
1164                     ''
1165                 )
1166             ),
1167             ''
1168         );
1169
1170         raw_text := REGEXP_REPLACE(raw_text, E'\\[.+?\\]', E'');
1171         raw_text := REGEXP_REPLACE(raw_text, E'\\mthe\\M|\\man?d?d\\M', E'', 'g'); -- arg! the pain!
1172
1173         IF idx.first_word IS TRUE THEN
1174             raw_text := REGEXP_REPLACE(raw_text, E'^(\\w+).*?$', E'\\1');
1175         END IF;
1176
1177                 output_text := output_text || REGEXP_REPLACE(raw_text, E'\\s+', '', 'g');
1178
1179         END LOOP;
1180
1181     RETURN output_text;
1182
1183 END;
1184 $func$ LANGUAGE PLPGSQL;
1185
1186 -- BEFORE UPDATE OR INSERT trigger for biblio.record_entry
1187 CREATE OR REPLACE FUNCTION biblio.fingerprint_trigger () RETURNS TRIGGER AS $func$
1188 BEGIN
1189
1190     -- For TG_ARGV, first param is language (like 'eng'), second is record type (like 'BKS')
1191
1192     IF NEW.deleted IS TRUE THEN -- we don't much care, then, do we?
1193         RETURN NEW;
1194     END IF;
1195
1196     NEW.fingerprint := biblio.extract_fingerprint(NEW.marc);
1197     NEW.quality := biblio.extract_quality(NEW.marc, TG_ARGV[0], TG_ARGV[1]);
1198
1199     RETURN NEW;
1200
1201 END;
1202 $func$ LANGUAGE PLPGSQL;
1203
1204 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_full_rec( bib_id BIGINT ) RETURNS VOID AS $func$
1205 BEGIN
1206     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
1207     IF NOT FOUND THEN
1208         DELETE FROM metabib.real_full_rec WHERE record = bib_id;
1209     END IF;
1210     INSERT INTO metabib.real_full_rec (record, tag, ind1, ind2, subfield, value)
1211         SELECT record, tag, ind1, ind2, subfield, value FROM biblio.flatten_marc( bib_id );
1212
1213     RETURN;
1214 END;
1215 $func$ LANGUAGE PLPGSQL;
1216
1217 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
1218 DECLARE
1219     uris            TEXT[];
1220     uri_xml         TEXT;
1221     uri_label       TEXT;
1222     uri_href        TEXT;
1223     uri_use         TEXT;
1224     uri_owner_list  TEXT[];
1225     uri_owner       TEXT;
1226     uri_owner_id    INT;
1227     uri_id          INT;
1228     uri_cn_id       INT;
1229     uri_map_id      INT;
1230 BEGIN
1231
1232     -- Clear any URI mappings and call numbers for this bib.
1233     -- This leads to acn / auricnm inflation, but also enables
1234     -- old acn/auricnm's to go away and for bibs to be deleted.
1235     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
1236         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
1237         DELETE FROM asset.call_number WHERE id = uri_cn_id;
1238     END LOOP;
1239
1240     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
1241     IF ARRAY_UPPER(uris,1) > 0 THEN
1242         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
1243             -- First we pull info out of the 856
1244             uri_xml     := uris[i];
1245
1246             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
1247             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()',uri_xml))[1];
1248             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
1249
1250             IF uri_label IS NULL THEN
1251                 uri_label := uri_href;
1252             END IF;
1253             CONTINUE WHEN uri_href IS NULL;
1254
1255             -- Get the distinct list of libraries wanting to use 
1256             SELECT  ARRAY_AGG(
1257                         DISTINCT REGEXP_REPLACE(
1258                             x,
1259                             $re$^.*?\((\w+)\).*$$re$,
1260                             E'\\1'
1261                         )
1262                     ) INTO uri_owner_list
1263               FROM  UNNEST(
1264                         oils_xpath(
1265                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
1266                             uri_xml
1267                         )
1268                     )x;
1269
1270             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
1271
1272                 -- look for a matching uri
1273                 IF uri_use IS NULL THEN
1274                     SELECT id INTO uri_id
1275                         FROM asset.uri
1276                         WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active
1277                         ORDER BY id LIMIT 1;
1278                     IF NOT FOUND THEN -- create one
1279                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
1280                         SELECT id INTO uri_id
1281                             FROM asset.uri
1282                             WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
1283                     END IF;
1284                 ELSE
1285                     SELECT id INTO uri_id
1286                         FROM asset.uri
1287                         WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active
1288                         ORDER BY id LIMIT 1;
1289                     IF NOT FOUND THEN -- create one
1290                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
1291                         SELECT id INTO uri_id
1292                             FROM asset.uri
1293                             WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
1294                     END IF;
1295                 END IF;
1296
1297                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
1298                     uri_owner := uri_owner_list[j];
1299
1300                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
1301                     CONTINUE WHEN NOT FOUND;
1302
1303                     -- we need a call number to link through
1304                     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;
1305                     IF NOT FOUND THEN
1306                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
1307                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
1308                         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;
1309                     END IF;
1310
1311                     -- now, link them if they're not already
1312                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
1313                     IF NOT FOUND THEN
1314                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
1315                     END IF;
1316
1317                 END LOOP;
1318
1319             END IF;
1320
1321         END LOOP;
1322     END IF;
1323
1324     RETURN;
1325 END;
1326 $func$ LANGUAGE PLPGSQL;
1327
1328 CREATE OR REPLACE FUNCTION metabib.remap_metarecord_for_bib( bib_id BIGINT, fp TEXT ) RETURNS BIGINT AS $func$
1329 DECLARE
1330     source_count    INT;
1331     old_mr          BIGINT;
1332     tmp_mr          metabib.metarecord%ROWTYPE;
1333     deleted_mrs     BIGINT[];
1334 BEGIN
1335
1336     DELETE FROM metabib.metarecord_source_map WHERE source = bib_id; -- Rid ourselves of the search-estimate-killing linkage
1337
1338     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
1339
1340         IF old_mr IS NULL AND fp = tmp_mr.fingerprint THEN -- Find the first fingerprint-matching
1341             old_mr := tmp_mr.id;
1342         ELSE
1343             SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id;
1344             IF source_count = 0 THEN -- No other records
1345                 deleted_mrs := ARRAY_APPEND(deleted_mrs, tmp_mr.id);
1346                 DELETE FROM metabib.metarecord WHERE id = tmp_mr.id;
1347             END IF;
1348         END IF;
1349
1350     END LOOP;
1351
1352     IF old_mr IS NULL THEN -- we found no suitable, preexisting MR based on old source maps
1353         SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp; -- is there one for our current fingerprint?
1354         IF old_mr IS NULL THEN -- nope, create one and grab its id
1355             INSERT INTO metabib.metarecord ( fingerprint, master_record ) VALUES ( fp, bib_id );
1356             SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp;
1357         ELSE -- indeed there is. update it with a null cache and recalcualated master record
1358             UPDATE  metabib.metarecord
1359               SET   mods = NULL,
1360                     master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp ORDER BY quality DESC LIMIT 1)
1361               WHERE id = old_mr;
1362         END IF;
1363     ELSE -- there was one we already attached to, update its mods cache and master_record
1364         UPDATE  metabib.metarecord
1365           SET   mods = NULL,
1366                 master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp ORDER BY quality DESC LIMIT 1)
1367           WHERE id = old_mr;
1368     END IF;
1369
1370     INSERT INTO metabib.metarecord_source_map (metarecord, source) VALUES (old_mr, bib_id); -- new source mapping
1371
1372     IF ARRAY_UPPER(deleted_mrs,1) > 0 THEN
1373         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
1374     END IF;
1375
1376     RETURN old_mr;
1377
1378 END;
1379 $func$ LANGUAGE PLPGSQL;
1380
1381 CREATE OR REPLACE FUNCTION biblio.map_authority_linking (bibid BIGINT, marc TEXT) RETURNS BIGINT AS $func$
1382     DELETE FROM authority.bib_linking WHERE bib = $1;
1383     INSERT INTO authority.bib_linking (bib, authority)
1384         SELECT  y.bib,
1385                 y.authority
1386           FROM (    SELECT  DISTINCT $1 AS bib,
1387                             BTRIM(remove_paren_substring(txt))::BIGINT AS authority
1388                       FROM  unnest(oils_xpath('//*[@code="0"]/text()',$2)) x(txt)
1389                       WHERE BTRIM(remove_paren_substring(txt)) ~ $re$^\d+$$re$
1390                 ) y JOIN authority.record_entry r ON r.id = y.authority;
1391     SELECT $1;
1392 $func$ LANGUAGE SQL;
1393
1394 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$
1395 DECLARE
1396     transformed_xml TEXT;
1397     rmarc           TEXT := prmarc;
1398     tmp_val         TEXT;
1399     prev_xfrm       TEXT;
1400     normalizer      RECORD;
1401     xfrm            config.xml_transform%ROWTYPE;
1402     attr_vector     INT[] := '{}'::INT[];
1403     attr_vector_tmp INT[];
1404     attr_list       TEXT[] := pattr_list;
1405     attr_value      TEXT[];
1406     norm_attr_value TEXT[];
1407     tmp_xml         XML;
1408     attr_def        config.record_attr_definition%ROWTYPE;
1409     ccvm_row        config.coded_value_map%ROWTYPE;
1410 BEGIN
1411
1412     IF attr_list IS NULL OR rdeleted THEN -- need to do the full dance on INSERT or undelete
1413         SELECT ARRAY_AGG(name) INTO attr_list FROM config.record_attr_definition;
1414     END IF;
1415
1416     IF rmarc IS NULL THEN
1417         SELECT marc INTO rmarc FROM biblio.record_entry WHERE id = rid;
1418     END IF;
1419
1420     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE NOT composite AND name = ANY( attr_list ) ORDER BY format LOOP
1421
1422         attr_value := '{}'::TEXT[];
1423         norm_attr_value := '{}'::TEXT[];
1424         attr_vector_tmp := '{}'::INT[];
1425
1426         SELECT * INTO ccvm_row FROM config.coded_value_map c WHERE c.ctype = attr_def.name LIMIT 1; 
1427
1428         -- tag+sf attrs only support SVF
1429         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1430             SELECT  ARRAY[ARRAY_TO_STRING(ARRAY_AGG(value), COALESCE(attr_def.joiner,' '))] INTO attr_value
1431               FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
1432               WHERE record = rid
1433                     AND tag LIKE attr_def.tag
1434                     AND CASE
1435                         WHEN attr_def.sf_list IS NOT NULL 
1436                             THEN POSITION(subfield IN attr_def.sf_list) > 0
1437                         ELSE TRUE
1438                     END
1439               GROUP BY tag
1440               ORDER BY tag
1441               LIMIT 1;
1442
1443         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1444             attr_value := vandelay.marc21_extract_fixed_field_list(rmarc, attr_def.fixed_field);
1445
1446             IF NOT attr_def.multi THEN
1447                 attr_value := ARRAY[attr_value[1]];
1448             END IF;
1449
1450         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1451
1452             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1453         
1454             -- See if we can skip the XSLT ... it's expensive
1455             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1456                 -- Can't skip the transform
1457                 IF xfrm.xslt <> '---' THEN
1458                     transformed_xml := oils_xslt_process(rmarc,xfrm.xslt);
1459                 ELSE
1460                     transformed_xml := rmarc;
1461                 END IF;
1462     
1463                 prev_xfrm := xfrm.name;
1464             END IF;
1465
1466             IF xfrm.name IS NULL THEN
1467                 -- just grab the marcxml (empty) transform
1468                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1469                 prev_xfrm := xfrm.name;
1470             END IF;
1471
1472             FOR tmp_xml IN SELECT XPATH(attr_def.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]) LOOP
1473                 tmp_val := oils_xpath_string(
1474                                 '//*',
1475                                 tmp_xml::TEXT,
1476                                 COALESCE(attr_def.joiner,' '),
1477                                 ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
1478                             );
1479                 IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
1480                     attr_value := attr_value || tmp_val;
1481                     EXIT WHEN NOT attr_def.multi;
1482                 END IF;
1483             END LOOP;
1484
1485         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1486             SELECT  ARRAY_AGG(m.value) INTO attr_value
1487               FROM  vandelay.marc21_physical_characteristics(rmarc) v
1488                     LEFT JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
1489               WHERE v.subfield = attr_def.phys_char_sf AND (m.value IS NOT NULL AND BTRIM(m.value) <> '')
1490                     AND ( ccvm_row.id IS NULL OR ( ccvm_row.id IS NOT NULL AND v.id IS NOT NULL) );
1491
1492             IF NOT attr_def.multi THEN
1493                 attr_value := ARRAY[attr_value[1]];
1494             END IF;
1495
1496         END IF;
1497
1498                 -- apply index normalizers to attr_value
1499         FOR tmp_val IN SELECT value FROM UNNEST(attr_value) x(value) LOOP
1500             FOR normalizer IN
1501                 SELECT  n.func AS func,
1502                         n.param_count AS param_count,
1503                         m.params AS params
1504                   FROM  config.index_normalizer n
1505                         JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1506                   WHERE attr = attr_def.name
1507                   ORDER BY m.pos LOOP
1508                     EXECUTE 'SELECT ' || normalizer.func || '(' ||
1509                     COALESCE( quote_literal( tmp_val ), 'NULL' ) ||
1510                         CASE
1511                             WHEN normalizer.param_count > 0
1512                                 THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1513                                 ELSE ''
1514                             END ||
1515                     ')' INTO tmp_val;
1516
1517             END LOOP;
1518             IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
1519                 norm_attr_value := norm_attr_value || tmp_val;
1520             END IF;
1521         END LOOP;
1522         
1523         IF attr_def.filter THEN
1524             -- Create unknown uncontrolled values and find the IDs of the values
1525             IF ccvm_row.id IS NULL THEN
1526                 FOR tmp_val IN SELECT value FROM UNNEST(norm_attr_value) x(value) LOOP
1527                     IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
1528                         BEGIN -- use subtransaction to isolate unique constraint violations
1529                             INSERT INTO metabib.uncontrolled_record_attr_value ( attr, value ) VALUES ( attr_def.name, tmp_val );
1530                         EXCEPTION WHEN unique_violation THEN END;
1531                     END IF;
1532                 END LOOP;
1533
1534                 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 );
1535             ELSE
1536                 SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM config.coded_value_map WHERE ctype = attr_def.name AND code = ANY( norm_attr_value );
1537             END IF;
1538
1539             -- Add the new value to the vector
1540             attr_vector := attr_vector || attr_vector_tmp;
1541         END IF;
1542
1543         IF attr_def.sorter AND norm_attr_value[1] IS NOT NULL THEN
1544             DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
1545             INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, norm_attr_value[1]);
1546         END IF;
1547
1548     END LOOP;
1549
1550 /* We may need to rewrite the vlist to contain
1551    the intersection of new values for requested
1552    attrs and old values for ignored attrs. To
1553    do this, we take the old attr vlist and
1554    subtract any values that are valid for the
1555    requested attrs, and then add back the new
1556    set of attr values. */
1557
1558     IF ARRAY_LENGTH(pattr_list, 1) > 0 THEN 
1559         SELECT vlist INTO attr_vector_tmp FROM metabib.record_attr_vector_list WHERE source = rid;
1560         SELECT attr_vector_tmp - ARRAY_AGG(id) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
1561         attr_vector := attr_vector || attr_vector_tmp;
1562     END IF;
1563
1564     -- On to composite attributes, now that the record attrs have been pulled.  Processed in name order, so later composite
1565     -- attributes can depend on earlier ones.
1566     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE composite AND name = ANY( attr_list ) ORDER BY name LOOP
1567
1568         FOR ccvm_row IN SELECT * FROM config.coded_value_map c WHERE c.ctype = attr_def.name ORDER BY value LOOP
1569
1570             tmp_val := metabib.compile_composite_attr( ccvm_row.id );
1571             CONTINUE WHEN tmp_val IS NULL OR tmp_val = ''; -- nothing to do
1572
1573             IF attr_def.filter THEN
1574                 IF attr_vector @@ tmp_val::query_int THEN
1575                     attr_vector = attr_vector + intset(ccvm_row.id);
1576                     EXIT WHEN NOT attr_def.multi;
1577                 END IF;
1578             END IF;
1579
1580             IF attr_def.sorter THEN
1581                 IF attr_vector ~~ tmp_val THEN
1582                     DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
1583                     INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, ccvm_row.code);
1584                 END IF;
1585             END IF;
1586
1587         END LOOP;
1588
1589     END LOOP;
1590
1591     IF ARRAY_LENGTH(attr_vector, 1) > 0 THEN
1592         IF rdeleted THEN -- initial insert OR revivication
1593             DELETE FROM metabib.record_attr_vector_list WHERE source = rid;
1594             INSERT INTO metabib.record_attr_vector_list (source, vlist) VALUES (rid, attr_vector);
1595         ELSE
1596             UPDATE metabib.record_attr_vector_list SET vlist = attr_vector WHERE source = rid;
1597         END IF;
1598     END IF;
1599
1600 END;
1601
1602 $func$ LANGUAGE PLPGSQL;
1603
1604
1605 -- AFTER UPDATE OR INSERT trigger for biblio.record_entry
1606 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
1607 BEGIN
1608
1609     IF NEW.deleted THEN -- If this bib is deleted
1610         PERFORM * FROM config.internal_flag WHERE
1611             name = 'ingest.metarecord_mapping.preserve_on_delete' AND enabled;
1612         IF NOT FOUND THEN
1613             -- One needs to keep these around to support searches
1614             -- with the #deleted modifier, so one should turn on the named
1615             -- internal flag for that functionality.
1616             DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id;
1617             DELETE FROM metabib.record_attr_vector_list WHERE source = NEW.id;
1618         END IF;
1619
1620         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
1621         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
1622         DELETE FROM metabib.browse_entry_def_map WHERE source = NEW.id; -- Don't auto-suggest deleted bibs
1623         RETURN NEW; -- and we're done
1624     END IF;
1625
1626     IF TG_OP = 'UPDATE' THEN -- re-ingest?
1627         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
1628
1629         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
1630             RETURN NEW;
1631         END IF;
1632     END IF;
1633
1634     -- Record authority linking
1635     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
1636     IF NOT FOUND THEN
1637         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
1638     END IF;
1639
1640     -- Flatten and insert the mfr data
1641     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
1642     IF NOT FOUND THEN
1643         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
1644
1645         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
1646         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
1647         IF NOT FOUND THEN
1648             PERFORM metabib.reingest_record_attributes(NEW.id, NULL, NEW.marc, TG_OP = 'INSERT' OR OLD.deleted);
1649         END IF;
1650     END IF;
1651
1652     -- Gather and insert the field entry data
1653     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
1654
1655     -- Located URI magic
1656     IF TG_OP = 'INSERT' THEN
1657         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1658         IF NOT FOUND THEN
1659             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1660         END IF;
1661     ELSE
1662         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1663         IF NOT FOUND THEN
1664             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1665         END IF;
1666     END IF;
1667
1668     -- (re)map metarecord-bib linking
1669     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
1670         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
1671         IF NOT FOUND THEN
1672             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1673         END IF;
1674     ELSE -- we're doing an update, and we're not deleted, remap
1675         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
1676         IF NOT FOUND THEN
1677             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1678         END IF;
1679     END IF;
1680
1681     RETURN NEW;
1682 END;
1683 $func$ LANGUAGE PLPGSQL;
1684
1685 CREATE OR REPLACE FUNCTION metabib.browse_normalize(facet_text TEXT, mapped_field INT) RETURNS TEXT AS $$
1686 DECLARE
1687     normalizer  RECORD;
1688 BEGIN
1689
1690     FOR normalizer IN
1691         SELECT  n.func AS func,
1692                 n.param_count AS param_count,
1693                 m.params AS params
1694           FROM  config.index_normalizer n
1695                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
1696           WHERE m.field = mapped_field AND m.pos < 0
1697           ORDER BY m.pos LOOP
1698
1699             EXECUTE 'SELECT ' || normalizer.func || '(' ||
1700                 quote_literal( facet_text ) ||
1701                 CASE
1702                     WHEN normalizer.param_count > 0
1703                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1704                         ELSE ''
1705                     END ||
1706                 ')' INTO facet_text;
1707
1708     END LOOP;
1709
1710     RETURN facet_text;
1711 END;
1712
1713 $$ LANGUAGE PLPGSQL;
1714
1715
1716 -- This mimics a specific part of QueryParser, turning the first part of a
1717 -- classed search (search_class) into a set of classes and possibly fields.
1718 -- search_class might look like "author" or "title|proper" or "ti|uniform"
1719 -- or "au" or "au|corporate|personal" or anything like that, where the first
1720 -- element of the list you get by separating on the "|" character is either
1721 -- a registered class (config.metabib_class) or an alias
1722 -- (config.metabib_search_alias), and the rest of any such elements are
1723 -- fields (config.metabib_field).
1724 CREATE OR REPLACE
1725     FUNCTION metabib.search_class_to_registered_components(search_class TEXT)
1726     RETURNS SETOF RECORD AS $func$
1727 DECLARE
1728     search_parts        TEXT[];
1729     field_name          TEXT;
1730     search_part_count   INTEGER;
1731     rec                 RECORD;
1732     registered_class    config.metabib_class%ROWTYPE;
1733     registered_alias    config.metabib_search_alias%ROWTYPE;
1734     registered_field    config.metabib_field%ROWTYPE;
1735 BEGIN
1736     search_parts := REGEXP_SPLIT_TO_ARRAY(search_class, E'\\|');
1737
1738     search_part_count := ARRAY_LENGTH(search_parts, 1);
1739     IF search_part_count = 0 THEN
1740         RETURN;
1741     ELSE
1742         SELECT INTO registered_class
1743             * FROM config.metabib_class WHERE name = search_parts[1];
1744         IF FOUND THEN
1745             IF search_part_count < 2 THEN   -- all fields
1746                 rec := (registered_class.name, NULL::INTEGER);
1747                 RETURN NEXT rec;
1748                 RETURN; -- done
1749             END IF;
1750             FOR field_name IN SELECT *
1751                 FROM UNNEST(search_parts[2:search_part_count]) LOOP
1752                 SELECT INTO registered_field
1753                     * FROM config.metabib_field
1754                     WHERE name = field_name AND
1755                         field_class = registered_class.name;
1756                 IF FOUND THEN
1757                     rec := (registered_class.name, registered_field.id);
1758                     RETURN NEXT rec;
1759                 END IF;
1760             END LOOP;
1761         ELSE
1762             -- maybe we have an alias?
1763             SELECT INTO registered_alias
1764                 * FROM config.metabib_search_alias WHERE alias=search_parts[1];
1765             IF NOT FOUND THEN
1766                 RETURN;
1767             ELSE
1768                 IF search_part_count < 2 THEN   -- return w/e the alias says
1769                     rec := (
1770                         registered_alias.field_class, registered_alias.field
1771                     );
1772                     RETURN NEXT rec;
1773                     RETURN; -- done
1774                 ELSE
1775                     FOR field_name IN SELECT *
1776                         FROM UNNEST(search_parts[2:search_part_count]) LOOP
1777                         SELECT INTO registered_field
1778                             * FROM config.metabib_field
1779                             WHERE name = field_name AND
1780                                 field_class = registered_alias.field_class;
1781                         IF FOUND THEN
1782                             rec := (
1783                                 registered_alias.field_class,
1784                                 registered_field.id
1785                             );
1786                             RETURN NEXT rec;
1787                         END IF;
1788                     END LOOP;
1789                 END IF;
1790             END IF;
1791         END IF;
1792     END IF;
1793 END;
1794 $func$ LANGUAGE PLPGSQL ROWS 1;
1795
1796
1797 -- Given a string such as a user might type into a search box, prepare
1798 -- two changed variants for TO_TSQUERY(). See
1799 -- http://www.postgresql.org/docs/9.0/static/textsearch-controls.html
1800 -- The first variant is normalized to match indexed documents regardless
1801 -- of diacritics.  The second variant keeps its diacritics for proper
1802 -- highlighting via TS_HEADLINE().
1803 CREATE OR REPLACE
1804     FUNCTION metabib.autosuggest_prepare_tsquery(orig TEXT) RETURNS TEXT[] AS
1805 $$
1806 DECLARE
1807     orig_ended_in_space     BOOLEAN;
1808     result                  RECORD;
1809     plain                   TEXT;
1810     normalized              TEXT;
1811 BEGIN
1812     orig_ended_in_space := orig ~ E'\\s$';
1813
1814     orig := ARRAY_TO_STRING(
1815         evergreen.regexp_split_to_array(orig, E'\\W+'), ' '
1816     );
1817
1818     normalized := public.naco_normalize(orig); -- also trim()s
1819     plain := trim(orig);
1820
1821     IF NOT orig_ended_in_space THEN
1822         plain := plain || ':*';
1823         normalized := normalized || ':*';
1824     END IF;
1825
1826     plain := ARRAY_TO_STRING(
1827         evergreen.regexp_split_to_array(plain, E'\\s+'), ' & '
1828     );
1829     normalized := ARRAY_TO_STRING(
1830         evergreen.regexp_split_to_array(normalized, E'\\s+'), ' & '
1831     );
1832
1833     RETURN ARRAY[normalized, plain];
1834 END;
1835 $$ LANGUAGE PLPGSQL;
1836
1837
1838 CREATE OR REPLACE
1839     FUNCTION metabib.suggest_browse_entries(
1840         raw_query_text  TEXT,   -- actually typed by humans at the UI level
1841         search_class    TEXT,   -- 'alias' or 'class' or 'class|field..', etc
1842         headline_opts   TEXT,   -- markup options for ts_headline()
1843         visibility_org  INTEGER,-- null if you don't want opac visibility test
1844         query_limit     INTEGER,-- use in LIMIT clause of interal query
1845         normalization   INTEGER -- argument to TS_RANK_CD()
1846     ) RETURNS TABLE (
1847         value                   TEXT,   -- plain
1848         field                   INTEGER,
1849         buoyant_and_class_match BOOL,
1850         field_match             BOOL,
1851         field_weight            INTEGER,
1852         rank                    REAL,
1853         buoyant                 BOOL,
1854         match                   TEXT    -- marked up
1855     ) AS $func$
1856 DECLARE
1857     prepared_query_texts    TEXT[];
1858     query                   TSQUERY;
1859     plain_query             TSQUERY;
1860     opac_visibility_join    TEXT;
1861     search_class_join       TEXT;
1862     r_fields                RECORD;
1863 BEGIN
1864     prepared_query_texts := metabib.autosuggest_prepare_tsquery(raw_query_text);
1865
1866     query := TO_TSQUERY('keyword', prepared_query_texts[1]);
1867     plain_query := TO_TSQUERY('keyword', prepared_query_texts[2]);
1868
1869     visibility_org := NULLIF(visibility_org,-1);
1870     IF visibility_org IS NOT NULL THEN
1871         opac_visibility_join := '
1872     JOIN asset.opac_visible_copies aovc ON (
1873         aovc.record = x.source AND
1874         aovc.circ_lib IN (SELECT id FROM actor.org_unit_descendants($4))
1875     )';
1876     ELSE
1877         opac_visibility_join := '';
1878     END IF;
1879
1880     -- The following determines whether we only provide suggestsons matching
1881     -- the user's selected search_class, or whether we show other suggestions
1882     -- too. The reason for MIN() is that for search_classes like
1883     -- 'title|proper|uniform' you would otherwise get multiple rows.  The
1884     -- implication is that if title as a class doesn't have restrict,
1885     -- nor does the proper field, but the uniform field does, you're going
1886     -- to get 'false' for your overall evaluation of 'should we restrict?'
1887     -- To invert that, change from MIN() to MAX().
1888
1889     SELECT
1890         INTO r_fields
1891             MIN(cmc.restrict::INT) AS restrict_class,
1892             MIN(cmf.restrict::INT) AS restrict_field
1893         FROM metabib.search_class_to_registered_components(search_class)
1894             AS _registered (field_class TEXT, field INT)
1895         JOIN
1896             config.metabib_class cmc ON (cmc.name = _registered.field_class)
1897         LEFT JOIN
1898             config.metabib_field cmf ON (cmf.id = _registered.field);
1899
1900     -- evaluate 'should we restrict?'
1901     IF r_fields.restrict_field::BOOL OR r_fields.restrict_class::BOOL THEN
1902         search_class_join := '
1903     JOIN
1904         metabib.search_class_to_registered_components($2)
1905         AS _registered (field_class TEXT, field INT) ON (
1906             (_registered.field IS NULL AND
1907                 _registered.field_class = cmf.field_class) OR
1908             (_registered.field = cmf.id)
1909         )
1910     ';
1911     ELSE
1912         search_class_join := '
1913     LEFT JOIN
1914         metabib.search_class_to_registered_components($2)
1915         AS _registered (field_class TEXT, field INT) ON (
1916             _registered.field_class = cmc.name
1917         )
1918     ';
1919     END IF;
1920
1921     RETURN QUERY EXECUTE '
1922 SELECT  DISTINCT
1923         x.value,
1924         x.id,
1925         x.push,
1926         x.restrict,
1927         x.weight,
1928         x.ts_rank_cd,
1929         x.buoyant,
1930         TS_HEADLINE(value, $7, $3)
1931   FROM  (SELECT DISTINCT
1932                 mbe.value,
1933                 cmf.id,
1934                 cmc.buoyant AND _registered.field_class IS NOT NULL AS push,
1935                 _registered.field = cmf.id AS restrict,
1936                 cmf.weight,
1937                 TS_RANK_CD(mbe.index_vector, $1, $6),
1938                 cmc.buoyant,
1939                 mbedm.source
1940           FROM  metabib.browse_entry_def_map mbedm
1941                 JOIN (SELECT * FROM metabib.browse_entry WHERE index_vector @@ $1 LIMIT 10000) mbe ON (mbe.id = mbedm.entry)
1942                 JOIN config.metabib_field cmf ON (cmf.id = mbedm.def)
1943                 JOIN config.metabib_class cmc ON (cmf.field_class = cmc.name)
1944                 '  || search_class_join || '
1945           ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
1946           LIMIT 1000) AS x
1947         ' || opac_visibility_join || '
1948   ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
1949   LIMIT $5
1950 '   -- sic, repeat the order by clause in the outer select too
1951     USING
1952         query, search_class, headline_opts,
1953         visibility_org, query_limit, normalization, plain_query
1954         ;
1955
1956     -- sort order:
1957     --  buoyant AND chosen class = match class
1958     --  chosen field = match field
1959     --  field weight
1960     --  rank
1961     --  buoyancy
1962     --  value itself
1963
1964 END;
1965 $func$ LANGUAGE PLPGSQL;
1966
1967 CREATE OR REPLACE FUNCTION public.oils_tsearch2 () RETURNS TRIGGER AS $$
1968 DECLARE
1969     normalizer      RECORD;
1970     value           TEXT := '';
1971     temp_vector     TEXT := '';
1972     ts_rec          RECORD;
1973     cur_weight      "char";
1974 BEGIN
1975
1976     value := NEW.value;
1977     NEW.index_vector = ''::tsvector;
1978
1979     IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
1980         FOR normalizer IN
1981             SELECT  n.func AS func,
1982                     n.param_count AS param_count,
1983                     m.params AS params
1984               FROM  config.index_normalizer n
1985                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
1986               WHERE field = NEW.field AND m.pos < 0
1987               ORDER BY m.pos LOOP
1988                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
1989                     quote_literal( value ) ||
1990                     CASE
1991                         WHEN normalizer.param_count > 0
1992                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1993                             ELSE ''
1994                         END ||
1995                     ')' INTO value;
1996
1997         END LOOP;
1998
1999         NEW.value = value;
2000
2001         FOR normalizer IN
2002             SELECT  n.func AS func,
2003                     n.param_count AS param_count,
2004                     m.params AS params
2005               FROM  config.index_normalizer n
2006                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
2007               WHERE field = NEW.field AND m.pos >= 0
2008               ORDER BY m.pos LOOP
2009                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
2010                     quote_literal( value ) ||
2011                     CASE
2012                         WHEN normalizer.param_count > 0
2013                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
2014                             ELSE ''
2015                         END ||
2016                     ')' INTO value;
2017
2018         END LOOP;
2019    END IF;
2020
2021     IF TG_TABLE_NAME::TEXT ~ 'browse_entry$' THEN
2022         value :=  ARRAY_TO_STRING(
2023             evergreen.regexp_split_to_array(value, E'\\W+'), ' '
2024         );
2025         value := public.search_normalize(value);
2026         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
2027     ELSIF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
2028         FOR ts_rec IN
2029             SELECT ts_config, index_weight
2030             FROM config.metabib_class_ts_map
2031             WHERE field_class = TG_ARGV[0]
2032                 AND index_lang IS NULL OR EXISTS (SELECT 1 FROM metabib.record_attr WHERE id = NEW.source AND index_lang IN(attrs->'item_lang',attrs->'language'))
2033                 AND always OR NOT EXISTS (SELECT 1 FROM config.metabib_field_ts_map WHERE metabib_field = NEW.field)
2034             UNION
2035             SELECT ts_config, index_weight
2036             FROM config.metabib_field_ts_map
2037             WHERE metabib_field = NEW.field
2038                AND index_lang IS NULL OR EXISTS (SELECT 1 FROM metabib.record_attr WHERE id = NEW.source AND index_lang IN(attrs->'item_lang',attrs->'language'))
2039             ORDER BY index_weight ASC
2040         LOOP
2041             IF cur_weight IS NOT NULL AND cur_weight != ts_rec.index_weight THEN
2042                 NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
2043                 temp_vector = '';
2044             END IF;
2045             cur_weight = ts_rec.index_weight;
2046             SELECT INTO temp_vector temp_vector || ' ' || to_tsvector(ts_rec.ts_config::regconfig, value)::TEXT;
2047         END LOOP;
2048         NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
2049     ELSE
2050         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
2051     END IF;
2052
2053     RETURN NEW;
2054 END;
2055 $$ LANGUAGE PLPGSQL;
2056
2057
2058 CREATE TYPE metabib.flat_browse_entry_appearance AS (
2059     browse_entry    BIGINT,
2060     value           TEXT,
2061     fields          TEXT,
2062     authorities     TEXT,
2063     sees            TEXT,
2064     sources         INT,        -- visible ones, that is
2065     asources        INT,        -- visible ones, that is
2066     row_number      INT,        -- internal use, sort of
2067     accurate        BOOL,       -- Count in sources field is accurate? Not
2068                                 -- if we had more than a browse superpage
2069                                 -- of records to look at.
2070     aaccurate       BOOL,       -- See previous comment...
2071     pivot_point     BIGINT
2072 );
2073
2074
2075 CREATE OR REPLACE FUNCTION metabib.browse_bib_pivot(
2076     INT[],
2077     TEXT
2078 ) RETURNS BIGINT AS $p$
2079     SELECT  mbe.id
2080       FROM  metabib.browse_entry mbe
2081             JOIN metabib.browse_entry_def_map mbedm ON (
2082                 mbedm.entry = mbe.id
2083                 AND mbedm.def = ANY($1)
2084             )
2085       WHERE mbe.sort_value >= public.naco_normalize($2)
2086       ORDER BY mbe.sort_value, mbe.value LIMIT 1;
2087 $p$ LANGUAGE SQL STABLE;
2088
2089 CREATE OR REPLACE FUNCTION metabib.browse_authority_pivot(
2090     INT[],
2091     TEXT
2092 ) RETURNS BIGINT AS $p$
2093     SELECT  mbe.id
2094       FROM  metabib.browse_entry mbe
2095             JOIN metabib.browse_entry_simple_heading_map mbeshm ON ( mbeshm.entry = mbe.id )
2096             JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2097             JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2098                 ash.atag = map.authority_field
2099                 AND map.metabib_field = ANY($1)
2100             )
2101       WHERE mbe.sort_value >= public.naco_normalize($2)
2102       ORDER BY mbe.sort_value, mbe.value LIMIT 1;
2103 $p$ LANGUAGE SQL STABLE;
2104
2105 CREATE OR REPLACE FUNCTION metabib.browse_authority_refs_pivot(
2106     INT[],
2107     TEXT
2108 ) RETURNS BIGINT AS $p$
2109     SELECT  mbe.id
2110       FROM  metabib.browse_entry mbe
2111             JOIN metabib.browse_entry_simple_heading_map mbeshm ON ( mbeshm.entry = mbe.id )
2112             JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2113             JOIN authority.control_set_auth_field_metabib_field_map_refs_only map ON (
2114                 ash.atag = map.authority_field
2115                 AND map.metabib_field = ANY($1)
2116             )
2117       WHERE mbe.sort_value >= public.naco_normalize($2)
2118       ORDER BY mbe.sort_value, mbe.value LIMIT 1;
2119 $p$ LANGUAGE SQL STABLE;
2120
2121 CREATE OR REPLACE FUNCTION metabib.browse_pivot(
2122     INT[],
2123     TEXT
2124 ) RETURNS BIGINT AS $p$
2125     SELECT  id FROM metabib.browse_entry
2126       WHERE id IN (
2127                 metabib.browse_bib_pivot($1, $2),
2128                 metabib.browse_authority_refs_pivot($1,$2) -- only look in 4xx, 5xx, 7xx of authority
2129             )
2130       ORDER BY sort_value, value LIMIT 1;
2131 $p$ LANGUAGE SQL STABLE;
2132
2133
2134 CREATE OR REPLACE FUNCTION metabib.staged_browse(
2135     query                   TEXT,
2136     fields                  INT[],
2137     context_org             INT,
2138     context_locations       INT[],
2139     staff                   BOOL,
2140     browse_superpage_size   INT,
2141     count_up_from_zero      BOOL,   -- if false, count down from -1
2142     result_limit            INT,
2143     next_pivot_pos          INT
2144 ) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
2145 DECLARE
2146     curs                    REFCURSOR;
2147     rec                     RECORD;
2148     qpfts_query             TEXT;
2149     aqpfts_query            TEXT;
2150     afields                 INT[];
2151     bfields                 INT[];
2152     result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
2153     results_skipped         INT := 0;
2154     row_counter             INT := 0;
2155     row_number              INT;
2156     slice_start             INT;
2157     slice_end               INT;
2158     full_end                INT;
2159     all_records             BIGINT[];
2160     all_brecords             BIGINT[];
2161     all_arecords            BIGINT[];
2162     superpage_of_records    BIGINT[];
2163     superpage_size          INT;
2164 BEGIN
2165     IF count_up_from_zero THEN
2166         row_number := 0;
2167     ELSE
2168         row_number := -1;
2169     END IF;
2170
2171     OPEN curs FOR EXECUTE query;
2172
2173     LOOP
2174         FETCH curs INTO rec;
2175         IF NOT FOUND THEN
2176             IF result_row.pivot_point IS NOT NULL THEN
2177                 RETURN NEXT result_row;
2178             END IF;
2179             RETURN;
2180         END IF;
2181
2182
2183         -- Gather aggregate data based on the MBE row we're looking at now, authority axis
2184         SELECT INTO all_arecords, result_row.sees, afields
2185                 ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
2186                 STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
2187                 ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
2188
2189           FROM  metabib.browse_entry_simple_heading_map mbeshm
2190                 JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2191                 JOIN authority.authority_linking aal ON ( ash.record = aal.source )
2192                 JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
2193                 JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2194                     ash.atag = map.authority_field
2195                     AND map.metabib_field = ANY(fields)
2196                 )
2197           WHERE mbeshm.entry = rec.id;
2198
2199
2200         -- Gather aggregate data based on the MBE row we're looking at now, bib axis
2201         SELECT INTO all_brecords, result_row.authorities, bfields
2202                 ARRAY_AGG(DISTINCT source),
2203                 STRING_AGG(DISTINCT authority::TEXT, $$,$$),
2204                 ARRAY_AGG(DISTINCT def)
2205           FROM  metabib.browse_entry_def_map
2206           WHERE entry = rec.id
2207                 AND def = ANY(fields);
2208
2209         SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
2210
2211         result_row.sources := 0;
2212         result_row.asources := 0;
2213
2214         -- Bib-linked vis checking
2215         IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
2216
2217             full_end := ARRAY_LENGTH(all_brecords, 1);
2218             superpage_size := COALESCE(browse_superpage_size, full_end);
2219             slice_start := 1;
2220             slice_end := superpage_size;
2221
2222             WHILE result_row.sources = 0 AND slice_start <= full_end LOOP
2223                 superpage_of_records := all_brecords[slice_start:slice_end];
2224                 qpfts_query :=
2225                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
2226                     '1::INT AS rel FROM (SELECT UNNEST(' ||
2227                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
2228
2229                 -- We use search.query_parser_fts() for visibility testing.
2230                 -- We're calling it once per browse-superpage worth of records
2231                 -- out of the set of records related to a given mbe, until we've
2232                 -- either exhausted that set of records or found at least 1
2233                 -- visible record.
2234
2235                 SELECT INTO result_row.sources visible
2236                     FROM search.query_parser_fts(
2237                         context_org, NULL, qpfts_query, NULL,
2238                         context_locations, 0, NULL, NULL, FALSE, staff, FALSE
2239                     ) qpfts
2240                     WHERE qpfts.rel IS NULL;
2241
2242                 slice_start := slice_start + superpage_size;
2243                 slice_end := slice_end + superpage_size;
2244             END LOOP;
2245
2246             -- Accurate?  Well, probably.
2247             result_row.accurate := browse_superpage_size IS NULL OR
2248                 browse_superpage_size >= full_end;
2249
2250         END IF;
2251
2252         -- Authority-linked vis checking
2253         IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
2254
2255             full_end := ARRAY_LENGTH(all_arecords, 1);
2256             superpage_size := COALESCE(browse_superpage_size, full_end);
2257             slice_start := 1;
2258             slice_end := superpage_size;
2259
2260             WHILE result_row.asources = 0 AND slice_start <= full_end LOOP
2261                 superpage_of_records := all_arecords[slice_start:slice_end];
2262                 qpfts_query :=
2263                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
2264                     '1::INT AS rel FROM (SELECT UNNEST(' ||
2265                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
2266
2267                 -- We use search.query_parser_fts() for visibility testing.
2268                 -- We're calling it once per browse-superpage worth of records
2269                 -- out of the set of records related to a given mbe, via
2270                 -- authority until we've either exhausted that set of records
2271                 -- or found at least 1 visible record.
2272
2273                 SELECT INTO result_row.asources visible
2274                     FROM search.query_parser_fts(
2275                         context_org, NULL, qpfts_query, NULL,
2276                         context_locations, 0, NULL, NULL, FALSE, staff, FALSE
2277                     ) qpfts
2278                     WHERE qpfts.rel IS NULL;
2279
2280                 slice_start := slice_start + superpage_size;
2281                 slice_end := slice_end + superpage_size;
2282             END LOOP;
2283
2284
2285             -- Accurate?  Well, probably.
2286             result_row.aaccurate := browse_superpage_size IS NULL OR
2287                 browse_superpage_size >= full_end;
2288
2289         END IF;
2290
2291         IF result_row.sources > 0 OR result_row.asources > 0 THEN
2292
2293             -- The function that calls this function needs row_number in order
2294             -- to correctly order results from two different runs of this
2295             -- functions.
2296             result_row.row_number := row_number;
2297
2298             -- Now, if row_counter is still less than limit, return a row.  If
2299             -- not, but it is less than next_pivot_pos, continue on without
2300             -- returning actual result rows until we find
2301             -- that next pivot, and return it.
2302
2303             IF row_counter < result_limit THEN
2304                 result_row.browse_entry := rec.id;
2305                 result_row.value := rec.value;
2306
2307                 RETURN NEXT result_row;
2308             ELSE
2309                 result_row.browse_entry := NULL;
2310                 result_row.authorities := NULL;
2311                 result_row.fields := NULL;
2312                 result_row.value := NULL;
2313                 result_row.sources := NULL;
2314                 result_row.sees := NULL;
2315                 result_row.accurate := NULL;
2316                 result_row.aaccurate := NULL;
2317                 result_row.pivot_point := rec.id;
2318
2319                 IF row_counter >= next_pivot_pos THEN
2320                     RETURN NEXT result_row;
2321                     RETURN;
2322                 END IF;
2323             END IF;
2324
2325             IF count_up_from_zero THEN
2326                 row_number := row_number + 1;
2327             ELSE
2328                 row_number := row_number - 1;
2329             END IF;
2330
2331             -- row_counter is different from row_number.
2332             -- It simply counts up from zero so that we know when
2333             -- we've reached our limit.
2334             row_counter := row_counter + 1;
2335         END IF;
2336     END LOOP;
2337 END;
2338 $p$ LANGUAGE PLPGSQL;
2339
2340
2341 CREATE OR REPLACE FUNCTION metabib.browse(
2342     search_field            INT[],
2343     browse_term             TEXT,
2344     context_org             INT DEFAULT NULL,
2345     context_loc_group       INT DEFAULT NULL,
2346     staff                   BOOL DEFAULT FALSE,
2347     pivot_id                BIGINT DEFAULT NULL,
2348     result_limit            INT DEFAULT 10
2349 ) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
2350 DECLARE
2351     core_query              TEXT;
2352     back_query              TEXT;
2353     forward_query           TEXT;
2354     pivot_sort_value        TEXT;
2355     pivot_sort_fallback     TEXT;
2356     context_locations       INT[];
2357     browse_superpage_size   INT;
2358     results_skipped         INT := 0;
2359     back_limit              INT;
2360     back_to_pivot           INT;
2361     forward_limit           INT;
2362     forward_to_pivot        INT;
2363 BEGIN
2364     -- First, find the pivot if we were given a browse term but not a pivot.
2365     IF pivot_id IS NULL THEN
2366         pivot_id := metabib.browse_pivot(search_field, browse_term);
2367     END IF;
2368
2369     SELECT INTO pivot_sort_value, pivot_sort_fallback
2370         sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
2371
2372     -- Bail if we couldn't find a pivot.
2373     IF pivot_sort_value IS NULL THEN
2374         RETURN;
2375     END IF;
2376
2377     -- Transform the context_loc_group argument (if any) (logc at the
2378     -- TPAC layer) into a form we'll be able to use.
2379     IF context_loc_group IS NOT NULL THEN
2380         SELECT INTO context_locations ARRAY_AGG(location)
2381             FROM asset.copy_location_group_map
2382             WHERE lgroup = context_loc_group;
2383     END IF;
2384
2385     -- Get the configured size of browse superpages.
2386     SELECT INTO browse_superpage_size value     -- NULL ok
2387         FROM config.global_flag
2388         WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
2389
2390     -- First we're going to search backward from the pivot, then we're going
2391     -- to search forward.  In each direction, we need two limits.  At the
2392     -- lesser of the two limits, we delineate the edge of the result set
2393     -- we're going to return.  At the greater of the two limits, we find the
2394     -- pivot value that would represent an offset from the current pivot
2395     -- at a distance of one "page" in either direction, where a "page" is a
2396     -- result set of the size specified in the "result_limit" argument.
2397     --
2398     -- The two limits in each direction make four derived values in total,
2399     -- and we calculate them now.
2400     back_limit := CEIL(result_limit::FLOAT / 2);
2401     back_to_pivot := result_limit;
2402     forward_limit := result_limit / 2;
2403     forward_to_pivot := result_limit - 1;
2404
2405     -- This is the meat of the SQL query that finds browse entries.  We'll
2406     -- pass this to a function which uses it with a cursor, so that individual
2407     -- rows may be fetched in a loop until some condition is satisfied, without
2408     -- waiting for a result set of fixed size to be collected all at once.
2409     core_query := '
2410 SELECT  mbe.id,
2411         mbe.value,
2412         mbe.sort_value
2413   FROM  metabib.browse_entry mbe
2414   WHERE (
2415             EXISTS ( -- are there any bibs using this mbe via the requested fields?
2416                 SELECT  1
2417                   FROM  metabib.browse_entry_def_map mbedm
2418                   WHERE mbedm.entry = mbe.id AND mbedm.def = ANY(' || quote_literal(search_field) || ')
2419                   LIMIT 1
2420             ) OR EXISTS ( -- are there any authorities using this mbe via the requested fields?
2421                 SELECT  1
2422                   FROM  metabib.browse_entry_simple_heading_map mbeshm
2423                         JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2424                         JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2425                             ash.atag = map.authority_field
2426                             AND map.metabib_field = ANY(' || quote_literal(search_field) || ')
2427                         )
2428                   WHERE mbeshm.entry = mbe.id
2429             )
2430         ) AND ';
2431
2432     -- This is the variant of the query for browsing backward.
2433     back_query := core_query ||
2434         ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
2435     ' ORDER BY mbe.sort_value DESC, mbe.value DESC ';
2436
2437     -- This variant browses forward.
2438     forward_query := core_query ||
2439         ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
2440     ' ORDER BY mbe.sort_value, mbe.value ';
2441
2442     -- We now call the function which applies a cursor to the provided
2443     -- queries, stopping at the appropriate limits and also giving us
2444     -- the next page's pivot.
2445     RETURN QUERY
2446         SELECT * FROM metabib.staged_browse(
2447             back_query, search_field, context_org, context_locations,
2448             staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
2449         ) UNION
2450         SELECT * FROM metabib.staged_browse(
2451             forward_query, search_field, context_org, context_locations,
2452             staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
2453         ) ORDER BY row_number DESC;
2454
2455 END;
2456 $p$ LANGUAGE PLPGSQL;
2457
2458 CREATE OR REPLACE FUNCTION metabib.browse(
2459     search_class        TEXT,
2460     browse_term         TEXT,
2461     context_org         INT DEFAULT NULL,
2462     context_loc_group   INT DEFAULT NULL,
2463     staff               BOOL DEFAULT FALSE,
2464     pivot_id            BIGINT DEFAULT NULL,
2465     result_limit        INT DEFAULT 10
2466 ) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
2467 BEGIN
2468     RETURN QUERY SELECT * FROM metabib.browse(
2469         (SELECT COALESCE(ARRAY_AGG(id), ARRAY[]::INT[])
2470             FROM config.metabib_field WHERE field_class = search_class),
2471         browse_term,
2472         context_org,
2473         context_loc_group,
2474         staff,
2475         pivot_id,
2476         result_limit
2477     );
2478 END;
2479 $p$ LANGUAGE PLPGSQL;
2480
2481 COMMIT;
2482