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