]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/030.schema.metabib.sql
LP#1303940: don't attempt to store NULL values in keyword, browse, or facet indexes
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 030.schema.metabib.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2007-2008  Equinox Software, Inc.
4  * Mike Rylander <miker@esilibrary.com> 
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  */
17
18 DROP SCHEMA IF EXISTS metabib CASCADE;
19
20 BEGIN;
21 CREATE SCHEMA metabib;
22
23 CREATE TABLE metabib.metarecord (
24         id              BIGSERIAL       PRIMARY KEY,
25         fingerprint     TEXT            NOT NULL,
26         master_record   BIGINT,
27         mods            TEXT
28 );
29 CREATE INDEX metabib_metarecord_master_record_idx ON metabib.metarecord (master_record);
30 CREATE INDEX metabib_metarecord_fingerprint_idx ON metabib.metarecord (fingerprint);
31
32 CREATE TABLE metabib.identifier_field_entry (
33         id              BIGSERIAL       PRIMARY KEY,
34         source          BIGINT          NOT NULL,
35         field           INT             NOT NULL,
36         value           TEXT            NOT NULL,
37         index_vector    tsvector        NOT NULL
38 );
39 CREATE TRIGGER metabib_identifier_field_entry_fti_trigger
40         BEFORE UPDATE OR INSERT ON metabib.identifier_field_entry
41         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('identifier');
42
43 CREATE INDEX metabib_identifier_field_entry_index_vector_idx ON metabib.identifier_field_entry USING GIST (index_vector);
44 CREATE INDEX metabib_identifier_field_entry_value_idx ON metabib.identifier_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
45 CREATE INDEX metabib_identifier_field_entry_source_idx ON metabib.identifier_field_entry (source);
46
47 CREATE TABLE metabib.combined_identifier_field_entry (
48         record          BIGINT          NOT NULL,
49         metabib_field           INT             NULL,
50         index_vector    tsvector        NOT NULL
51 );
52 CREATE UNIQUE INDEX metabib_combined_identifier_field_entry_fakepk_idx ON metabib.combined_identifier_field_entry (record, COALESCE(metabib_field::TEXT,''));
53 CREATE INDEX metabib_combined_identifier_field_entry_index_vector_idx ON metabib.combined_identifier_field_entry USING GIST (index_vector);
54 CREATE INDEX metabib_combined_identifier_field_source_idx ON metabib.combined_identifier_field_entry (metabib_field);
55
56 CREATE TABLE metabib.title_field_entry (
57         id              BIGSERIAL       PRIMARY KEY,
58         source          BIGINT          NOT NULL,
59         field           INT             NOT NULL,
60         value           TEXT            NOT NULL,
61         index_vector    tsvector        NOT NULL
62 );
63 CREATE TRIGGER metabib_title_field_entry_fti_trigger
64         BEFORE UPDATE OR INSERT ON metabib.title_field_entry
65         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('title');
66
67 CREATE INDEX metabib_title_field_entry_index_vector_idx ON metabib.title_field_entry USING GIST (index_vector);
68 CREATE INDEX metabib_title_field_entry_value_idx ON metabib.title_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
69 CREATE INDEX metabib_title_field_entry_source_idx ON metabib.title_field_entry (source);
70
71 CREATE TABLE metabib.combined_title_field_entry (
72         record          BIGINT          NOT NULL,
73         metabib_field           INT             NULL,
74         index_vector    tsvector        NOT NULL
75 );
76 CREATE UNIQUE INDEX metabib_combined_title_field_entry_fakepk_idx ON metabib.combined_title_field_entry (record, COALESCE(metabib_field::TEXT,''));
77 CREATE INDEX metabib_combined_title_field_entry_index_vector_idx ON metabib.combined_title_field_entry USING GIST (index_vector);
78 CREATE INDEX metabib_combined_title_field_source_idx ON metabib.combined_title_field_entry (metabib_field);
79
80 CREATE TABLE metabib.author_field_entry (
81         id              BIGSERIAL       PRIMARY KEY,
82         source          BIGINT          NOT NULL,
83         field           INT             NOT NULL,
84         value           TEXT            NOT NULL,
85         index_vector    tsvector        NOT NULL
86 );
87 CREATE TRIGGER metabib_author_field_entry_fti_trigger
88         BEFORE UPDATE OR INSERT ON metabib.author_field_entry
89         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('author');
90
91 CREATE INDEX metabib_author_field_entry_index_vector_idx ON metabib.author_field_entry USING GIST (index_vector);
92 CREATE INDEX metabib_author_field_entry_value_idx ON metabib.author_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
93 CREATE INDEX metabib_author_field_entry_source_idx ON metabib.author_field_entry (source);
94
95 CREATE TABLE metabib.combined_author_field_entry (
96         record          BIGINT          NOT NULL,
97         metabib_field           INT             NULL,
98         index_vector    tsvector        NOT NULL
99 );
100 CREATE UNIQUE INDEX metabib_combined_author_field_entry_fakepk_idx ON metabib.combined_author_field_entry (record, COALESCE(metabib_field::TEXT,''));
101 CREATE INDEX metabib_combined_author_field_entry_index_vector_idx ON metabib.combined_author_field_entry USING GIST (index_vector);
102 CREATE INDEX metabib_combined_author_field_source_idx ON metabib.combined_author_field_entry (metabib_field);
103
104 CREATE TABLE metabib.subject_field_entry (
105         id              BIGSERIAL       PRIMARY KEY,
106         source          BIGINT          NOT NULL,
107         field           INT             NOT NULL,
108         value           TEXT            NOT NULL,
109         index_vector    tsvector        NOT NULL
110 );
111 CREATE TRIGGER metabib_subject_field_entry_fti_trigger
112         BEFORE UPDATE OR INSERT ON metabib.subject_field_entry
113         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('subject');
114
115 CREATE INDEX metabib_subject_field_entry_index_vector_idx ON metabib.subject_field_entry USING GIST (index_vector);
116 CREATE INDEX metabib_subject_field_entry_value_idx ON metabib.subject_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
117 CREATE INDEX metabib_subject_field_entry_source_idx ON metabib.subject_field_entry (source);
118
119 CREATE TABLE metabib.combined_subject_field_entry (
120         record          BIGINT          NOT NULL,
121         metabib_field           INT             NULL,
122         index_vector    tsvector        NOT NULL
123 );
124 CREATE UNIQUE INDEX metabib_combined_subject_field_entry_fakepk_idx ON metabib.combined_subject_field_entry (record, COALESCE(metabib_field::TEXT,''));
125 CREATE INDEX metabib_combined_subject_field_entry_index_vector_idx ON metabib.combined_subject_field_entry USING GIST (index_vector);
126 CREATE INDEX metabib_combined_subject_field_source_idx ON metabib.combined_subject_field_entry (metabib_field);
127
128 CREATE TABLE metabib.keyword_field_entry (
129         id              BIGSERIAL       PRIMARY KEY,
130         source          BIGINT          NOT NULL,
131         field           INT             NOT NULL,
132         value           TEXT            NOT NULL,
133         index_vector    tsvector        NOT NULL
134 );
135 CREATE TRIGGER metabib_keyword_field_entry_fti_trigger
136         BEFORE UPDATE OR INSERT ON metabib.keyword_field_entry
137         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
138
139 CREATE INDEX metabib_keyword_field_entry_index_vector_idx ON metabib.keyword_field_entry USING GIST (index_vector);
140 CREATE INDEX metabib_keyword_field_entry_value_idx ON metabib.keyword_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
141 CREATE INDEX metabib_keyword_field_entry_source_idx ON metabib.keyword_field_entry (source);
142
143 CREATE TABLE metabib.combined_keyword_field_entry (
144         record          BIGINT          NOT NULL,
145         metabib_field           INT             NULL,
146         index_vector    tsvector        NOT NULL
147 );
148 CREATE UNIQUE INDEX metabib_combined_keyword_field_entry_fakepk_idx ON metabib.combined_keyword_field_entry (record, COALESCE(metabib_field::TEXT,''));
149 CREATE INDEX metabib_combined_keyword_field_entry_index_vector_idx ON metabib.combined_keyword_field_entry USING GIST (index_vector);
150 CREATE INDEX metabib_combined_keyword_field_source_idx ON metabib.combined_keyword_field_entry (metabib_field);
151
152 CREATE TABLE metabib.series_field_entry (
153         id              BIGSERIAL       PRIMARY KEY,
154         source          BIGINT          NOT NULL,
155         field           INT             NOT NULL,
156         value           TEXT            NOT NULL,
157         index_vector    tsvector        NOT NULL
158 );
159 CREATE TRIGGER metabib_series_field_entry_fti_trigger
160         BEFORE UPDATE OR INSERT ON metabib.series_field_entry
161         FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('series');
162
163 CREATE INDEX metabib_series_field_entry_index_vector_idx ON metabib.series_field_entry USING GIST (index_vector);
164 CREATE INDEX metabib_series_field_entry_value_idx ON metabib.series_field_entry (SUBSTRING(value,1,1024)) WHERE index_vector = ''::TSVECTOR;
165 CREATE INDEX metabib_series_field_entry_source_idx ON metabib.series_field_entry (source);
166
167 CREATE TABLE metabib.combined_series_field_entry (
168         record          BIGINT          NOT NULL,
169         metabib_field           INT             NULL,
170         index_vector    tsvector        NOT NULL
171 );
172 CREATE UNIQUE INDEX metabib_combined_series_field_entry_fakepk_idx ON metabib.combined_series_field_entry (record, COALESCE(metabib_field::TEXT,''));
173 CREATE INDEX metabib_combined_series_field_entry_index_vector_idx ON metabib.combined_series_field_entry USING GIST (index_vector);
174 CREATE INDEX metabib_combined_series_field_source_idx ON metabib.combined_series_field_entry (metabib_field);
175
176 CREATE TABLE metabib.facet_entry (
177         id              BIGSERIAL       PRIMARY KEY,
178         source          BIGINT          NOT NULL,
179         field           INT             NOT NULL,
180         value           TEXT            NOT NULL
181 );
182 CREATE INDEX metabib_facet_entry_field_idx ON metabib.facet_entry (field);
183 CREATE INDEX metabib_facet_entry_value_idx ON metabib.facet_entry (SUBSTRING(value,1,1024));
184 CREATE INDEX metabib_facet_entry_source_idx ON metabib.facet_entry (source);
185
186 CREATE TABLE metabib.browse_entry (
187     id BIGSERIAL PRIMARY KEY,
188     value TEXT,
189     index_vector tsvector,
190     sort_value  TEXT NOT NULL,
191     UNIQUE(sort_value, value)
192 );
193
194
195 CREATE INDEX browse_entry_sort_value_idx
196     ON metabib.browse_entry USING BTREE (sort_value);
197
198 CREATE INDEX metabib_browse_entry_index_vector_idx ON metabib.browse_entry USING GIN (index_vector);
199 CREATE TRIGGER metabib_browse_entry_fti_trigger
200     BEFORE INSERT OR UPDATE ON metabib.browse_entry
201     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
202
203
204 CREATE TABLE metabib.browse_entry_def_map (
205     id BIGSERIAL PRIMARY KEY,
206     entry BIGINT REFERENCES metabib.browse_entry (id),
207     def INT REFERENCES config.metabib_field (id) ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
208     source BIGINT REFERENCES biblio.record_entry (id),
209     authority BIGINT REFERENCES authority.record_entry (id) ON DELETE SET NULL
210 );
211 CREATE INDEX browse_entry_def_map_def_idx ON metabib.browse_entry_def_map (def);
212 CREATE INDEX browse_entry_def_map_entry_idx ON metabib.browse_entry_def_map (entry);
213 CREATE INDEX browse_entry_def_map_source_idx ON metabib.browse_entry_def_map (source);
214
215 CREATE TABLE metabib.browse_entry_simple_heading_map (
216     id BIGSERIAL PRIMARY KEY,
217     entry BIGINT REFERENCES metabib.browse_entry (id),
218     simple_heading BIGINT REFERENCES authority.simple_heading (id) ON DELETE CASCADE
219 );
220 CREATE INDEX browse_entry_sh_map_entry_idx ON metabib.browse_entry_simple_heading_map (entry);
221 CREATE INDEX browse_entry_sh_map_sh_idx ON metabib.browse_entry_simple_heading_map (simple_heading);
222
223 CREATE OR REPLACE FUNCTION metabib.facet_normalize_trigger () RETURNS TRIGGER AS $$
224 DECLARE
225     normalizer  RECORD;
226     facet_text  TEXT;
227 BEGIN
228     facet_text := NEW.value;
229
230     FOR normalizer IN
231         SELECT  n.func AS func,
232                 n.param_count AS param_count,
233                 m.params AS params
234           FROM  config.index_normalizer n
235                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
236           WHERE m.field = NEW.field AND m.pos < 0
237           ORDER BY m.pos LOOP
238
239             EXECUTE 'SELECT ' || normalizer.func || '(' ||
240                 quote_literal( facet_text ) ||
241                 CASE
242                     WHEN normalizer.param_count > 0
243                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
244                         ELSE ''
245                     END ||
246                 ')' INTO facet_text;
247
248     END LOOP;
249
250     NEW.value = facet_text;
251
252     RETURN NEW;
253 END;
254 $$ LANGUAGE PLPGSQL;
255
256 CREATE TRIGGER facet_normalize_tgr
257         BEFORE UPDATE OR INSERT ON metabib.facet_entry
258         FOR EACH ROW EXECUTE PROCEDURE metabib.facet_normalize_trigger();
259
260 CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
261 BEGIN
262     NEW.value := force_unicode_normal_form(NEW.value,'NFC');
263     RETURN NEW;
264 END;
265 $$ LANGUAGE PLPGSQL;
266
267 CREATE TRIGGER facet_force_nfc_tgr
268         BEFORE UPDATE OR INSERT ON metabib.facet_entry
269         FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
270
271 -- DECREMENTING serial starts at -1
272 CREATE SEQUENCE metabib.uncontrolled_record_attr_value_id_seq INCREMENT BY -1;
273
274 CREATE TABLE metabib.uncontrolled_record_attr_value (
275     id      BIGINT  PRIMARY KEY DEFAULT nextval('metabib.uncontrolled_record_attr_value_id_seq'),
276     attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name),
277     value   TEXT    NOT NULL
278 );
279 CREATE UNIQUE INDEX muv_once_idx ON metabib.uncontrolled_record_attr_value (attr,value);
280
281 CREATE VIEW metabib.record_attr_id_map AS
282     SELECT id, attr, value FROM metabib.uncontrolled_record_attr_value
283         UNION
284     SELECT  c.id, c.ctype AS attr, c.code AS value
285       FROM  config.coded_value_map c
286             JOIN config.record_attr_definition d ON (d.name = c.ctype AND NOT d.composite);
287
288 CREATE VIEW metabib.composite_attr_id_map AS
289     SELECT  c.id, c.ctype AS attr, c.code AS value
290       FROM  config.coded_value_map c
291             JOIN config.record_attr_definition d ON (d.name = c.ctype AND d.composite);
292
293 CREATE VIEW metabib.full_attr_id_map AS
294     SELECT id, attr, value FROM metabib.record_attr_id_map
295         UNION
296     SELECT id, attr, value FROM metabib.composite_attr_id_map;
297
298
299 CREATE OR REPLACE FUNCTION metabib.compile_composite_attr_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
813         -- don't store what has been normalized away
814         CONTINUE WHEN ind_data.value IS NULL;
815
816         IF ind_data.field < 0 THEN
817             ind_data.field = -1 * ind_data.field;
818         END IF;
819
820         IF ind_data.facet_field AND NOT b_skip_facet THEN
821             INSERT INTO metabib.facet_entry (field, source, value)
822                 VALUES (ind_data.field, ind_data.source, ind_data.value);
823         END IF;
824
825         IF ind_data.browse_field AND NOT b_skip_browse THEN
826             -- A caveat about this SELECT: this should take care of replacing
827             -- old mbe rows when data changes, but not if normalization (by
828             -- which I mean specifically the output of
829             -- evergreen.oils_tsearch2()) changes.  It may or may not be
830             -- expensive to add a comparison of index_vector to index_vector
831             -- to the WHERE clause below.
832
833             CONTINUE WHEN ind_data.sort_value IS NULL;
834
835             value_prepped := metabib.browse_normalize(ind_data.value, ind_data.field);
836             SELECT INTO mbe_row * FROM metabib.browse_entry
837                 WHERE value = value_prepped AND sort_value = ind_data.sort_value;
838
839             IF FOUND THEN
840                 mbe_id := mbe_row.id;
841             ELSE
842                 INSERT INTO metabib.browse_entry
843                     ( value, sort_value ) VALUES
844                     ( value_prepped, ind_data.sort_value );
845
846                 mbe_id := CURRVAL('metabib.browse_entry_id_seq'::REGCLASS);
847             END IF;
848
849             INSERT INTO metabib.browse_entry_def_map (entry, def, source, authority)
850                 VALUES (mbe_id, ind_data.field, ind_data.source, ind_data.authority);
851         END IF;
852
853         IF ind_data.search_field AND NOT b_skip_search THEN
854             -- Avoid inserting duplicate rows
855             EXECUTE 'SELECT 1 FROM metabib.' || ind_data.field_class ||
856                 '_field_entry WHERE field = $1 AND source = $2 AND value = $3'
857                 INTO mbe_id USING ind_data.field, ind_data.source, ind_data.value;
858                 -- RAISE NOTICE 'Search for an already matching row returned %', mbe_id;
859             IF mbe_id IS NULL THEN
860                 EXECUTE $$
861                 INSERT INTO metabib.$$ || ind_data.field_class || $$_field_entry (field, source, value)
862                     VALUES ($$ ||
863                         quote_literal(ind_data.field) || $$, $$ ||
864                         quote_literal(ind_data.source) || $$, $$ ||
865                         quote_literal(ind_data.value) ||
866                     $$);$$;
867             END IF;
868         END IF;
869
870     END LOOP;
871
872     IF NOT b_skip_search THEN
873         PERFORM metabib.update_combined_index_vectors(bib_id);
874     END IF;
875
876     RETURN;
877 END;
878 $func$ LANGUAGE PLPGSQL;
879
880 -- default to a space joiner
881 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry ( BIGINT ) RETURNS SETOF metabib.field_entry_template AS $func$
882         SELECT * FROM biblio.extract_metabib_field_entry($1, ' ');
883 $func$ LANGUAGE SQL;
884
885 CREATE OR REPLACE FUNCTION authority.flatten_marc ( rid BIGINT ) RETURNS SETOF authority.full_rec AS $func$
886 DECLARE
887         auth    authority.record_entry%ROWTYPE;
888         output  authority.full_rec%ROWTYPE;
889         field   RECORD;
890 BEGIN
891         SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
892
893         FOR field IN SELECT * FROM vandelay.flatten_marc( auth.marc ) LOOP
894                 output.record := rid;
895                 output.ind1 := field.ind1;
896                 output.ind2 := field.ind2;
897                 output.tag := field.tag;
898                 output.subfield := field.subfield;
899                 output.value := field.value;
900
901                 RETURN NEXT output;
902         END LOOP;
903 END;
904 $func$ LANGUAGE PLPGSQL;
905
906 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
907 DECLARE
908         bib     biblio.record_entry%ROWTYPE;
909         output  metabib.full_rec%ROWTYPE;
910         field   RECORD;
911 BEGIN
912         SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
913
914         FOR field IN SELECT * FROM vandelay.flatten_marc( bib.marc ) LOOP
915                 output.record := rid;
916                 output.ind1 := field.ind1;
917                 output.ind2 := field.ind2;
918                 output.tag := field.tag;
919                 output.subfield := field.subfield;
920                 output.value := field.value;
921
922                 RETURN NEXT output;
923         END LOOP;
924 END;
925 $func$ LANGUAGE PLPGSQL;
926
927 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
928 DECLARE
929         ldr         TEXT;
930         tval        TEXT;
931         tval_rec    RECORD;
932         bval        TEXT;
933         bval_rec    RECORD;
934     retval      config.marc21_rec_type_map%ROWTYPE;
935 BEGIN
936     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
937
938     IF ldr IS NULL OR ldr = '' THEN
939         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
940         RETURN retval;
941     END IF;
942
943     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
944     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
945
946
947     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
948     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
949
950     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
951
952     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
953
954
955     IF retval.code IS NULL THEN
956         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
957     END IF;
958
959     RETURN retval;
960 END;
961 $func$ LANGUAGE PLPGSQL;
962
963 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
964     SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
965 $func$ LANGUAGE SQL;
966
967 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field_list( marc TEXT, ff TEXT ) RETURNS TEXT[] AS $func$
968 DECLARE
969     rtype       TEXT;
970     ff_pos      RECORD;
971     tag_data    RECORD;
972     val         TEXT;
973     collection  TEXT[] := '{}'::TEXT[];
974 BEGIN
975     rtype := (vandelay.marc21_record_type( marc )).code;
976     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
977         IF ff_pos.tag = 'ldr' THEN
978             val := oils_xpath_string('//*[local-name()="leader"]', marc);
979             IF val IS NOT NULL THEN
980                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
981                 collection := collection || val;
982             END IF;
983         ELSE
984             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
985                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
986                 collection := collection || val;
987             END LOOP;
988         END IF;
989         val := REPEAT( ff_pos.default_val, ff_pos.length );
990         collection := collection || val;
991     END LOOP;
992
993     RETURN collection;
994 END;
995 $func$ LANGUAGE PLPGSQL;
996
997 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
998 DECLARE
999     rtype       TEXT;
1000     ff_pos      RECORD;
1001     tag_data    RECORD;
1002     val         TEXT;
1003 BEGIN
1004     rtype := (vandelay.marc21_record_type( marc )).code;
1005     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
1006         IF ff_pos.tag = 'ldr' THEN
1007             val := oils_xpath_string('//*[local-name()="leader"]', marc);
1008             IF val IS NOT NULL THEN
1009                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
1010                 RETURN val;
1011             END IF;
1012         ELSE
1013             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1014                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1015                 RETURN val;
1016             END LOOP;
1017         END IF;
1018         val := REPEAT( ff_pos.default_val, ff_pos.length );
1019         RETURN val;
1020     END LOOP;
1021
1022     RETURN NULL;
1023 END;
1024 $func$ LANGUAGE PLPGSQL;
1025
1026 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field_list( rid BIGINT, ff TEXT ) RETURNS TEXT[] AS $func$
1027     SELECT * FROM vandelay.marc21_extract_fixed_field_list( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
1028 $func$ LANGUAGE SQL;
1029
1030 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
1031     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
1032 $func$ LANGUAGE SQL;
1033
1034 -- CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
1035 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
1036 DECLARE
1037     tag_data    TEXT;
1038     rtype       TEXT;
1039     ff_pos      RECORD;
1040     output      biblio.record_ff_map%ROWTYPE;
1041 BEGIN
1042     rtype := (vandelay.marc21_record_type( marc )).code;
1043
1044     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
1045         output.ff_name  := ff_pos.fixed_field;
1046         output.ff_value := NULL;
1047
1048         IF ff_pos.tag = 'ldr' THEN
1049             output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
1050             IF output.ff_value IS NOT NULL THEN
1051                 output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
1052                 RETURN NEXT output;
1053                 output.ff_value := NULL;
1054             END IF;
1055         ELSE
1056             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1057                 output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
1058                 IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
1059                 RETURN NEXT output;
1060                 output.ff_value := NULL;
1061             END LOOP;
1062         END IF;
1063
1064     END LOOP;
1065
1066     RETURN;
1067 END;
1068 $func$ LANGUAGE PLPGSQL;
1069
1070 CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
1071     SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1072 $func$ LANGUAGE SQL;
1073
1074 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1075     SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1076 $func$ LANGUAGE SQL;
1077
1078 CREATE OR REPLACE FUNCTION biblio.extract_quality ( marc TEXT, best_lang TEXT, best_type TEXT ) RETURNS INT AS $func$
1079 DECLARE
1080     qual        INT;
1081     ldr         TEXT;
1082     tval        TEXT;
1083     tval_rec    RECORD;
1084     bval        TEXT;
1085     bval_rec    RECORD;
1086     type_map    RECORD;
1087     ff_pos      RECORD;
1088     ff_tag_data TEXT;
1089 BEGIN
1090
1091     IF marc IS NULL OR marc = '' THEN
1092         RETURN NULL;
1093     END IF;
1094
1095     -- First, the count of tags
1096     qual := ARRAY_UPPER(oils_xpath('*[local-name()="datafield"]', marc), 1);
1097
1098     -- now go through a bunch of pain to get the record type
1099     IF best_type IS NOT NULL THEN
1100         ldr := (oils_xpath('//*[local-name()="leader"]/text()', marc))[1];
1101
1102         IF ldr IS NOT NULL THEN
1103             SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
1104             SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
1105
1106
1107             tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1108             bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1109
1110             -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1111
1112             SELECT * INTO type_map FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1113
1114             IF type_map.code IS NOT NULL THEN
1115                 IF best_type = type_map.code THEN
1116                     qual := qual + qual / 2;
1117                 END IF;
1118
1119                 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
1120                     ff_tag_data := SUBSTRING((oils_xpath('//*[@tag="' || ff_pos.tag || '"]/text()',marc))[1], ff_pos.start_pos + 1, ff_pos.length);
1121                     IF ff_tag_data = best_lang THEN
1122                             qual := qual + 100;
1123                     END IF;
1124                 END LOOP;
1125             END IF;
1126         END IF;
1127     END IF;
1128
1129     -- Now look for some quality metrics
1130     -- DCL record?
1131     IF ARRAY_UPPER(oils_xpath('//*[@tag="040"]/*[@code="a" and contains(.,"DLC")]', marc), 1) = 1 THEN
1132         qual := qual + 10;
1133     END IF;
1134
1135     -- From OCLC?
1136     IF (oils_xpath('//*[@tag="003"]/text()', marc))[1] ~* E'oclo?c' THEN
1137         qual := qual + 10;
1138     END IF;
1139
1140     RETURN qual;
1141
1142 END;
1143 $func$ LANGUAGE PLPGSQL;
1144
1145 CREATE OR REPLACE FUNCTION biblio.extract_fingerprint ( marc text ) RETURNS TEXT AS $func$
1146 DECLARE
1147         idx             config.biblio_fingerprint%ROWTYPE;
1148         xfrm            config.xml_transform%ROWTYPE;
1149         prev_xfrm       TEXT;
1150         transformed_xml TEXT;
1151         xml_node        TEXT;
1152         xml_node_list   TEXT[];
1153         raw_text        TEXT;
1154     output_text TEXT := '';
1155 BEGIN
1156
1157     IF marc IS NULL OR marc = '' THEN
1158         RETURN NULL;
1159     END IF;
1160
1161         -- Loop over the indexing entries
1162         FOR idx IN SELECT * FROM config.biblio_fingerprint ORDER BY format, id LOOP
1163
1164                 SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
1165
1166                 -- See if we can skip the XSLT ... it's expensive
1167                 IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1168                         -- Can't skip the transform
1169                         IF xfrm.xslt <> '---' THEN
1170                                 transformed_xml := oils_xslt_process(marc,xfrm.xslt);
1171                         ELSE
1172                                 transformed_xml := marc;
1173                         END IF;
1174
1175                         prev_xfrm := xfrm.name;
1176                 END IF;
1177
1178                 raw_text := COALESCE(
1179             naco_normalize(
1180                 ARRAY_TO_STRING(
1181                     oils_xpath(
1182                         '//text()',
1183                         (oils_xpath(
1184                             idx.xpath,
1185                             transformed_xml,
1186                             ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] 
1187                         ))[1]
1188                     ),
1189                     ''
1190                 )
1191             ),
1192             ''
1193         );
1194
1195         raw_text := REGEXP_REPLACE(raw_text, E'\\[.+?\\]', E'');
1196         raw_text := REGEXP_REPLACE(raw_text, E'\\mthe\\M|\\man?d?d\\M', E'', 'g'); -- arg! the pain!
1197
1198         IF idx.first_word IS TRUE THEN
1199             raw_text := REGEXP_REPLACE(raw_text, E'^(\\w+).*?$', E'\\1');
1200         END IF;
1201
1202                 output_text := output_text || REGEXP_REPLACE(raw_text, E'\\s+', '', 'g');
1203
1204         END LOOP;
1205
1206     RETURN output_text;
1207
1208 END;
1209 $func$ LANGUAGE PLPGSQL;
1210
1211 -- BEFORE UPDATE OR INSERT trigger for biblio.record_entry
1212 CREATE OR REPLACE FUNCTION biblio.fingerprint_trigger () RETURNS TRIGGER AS $func$
1213 BEGIN
1214
1215     -- For TG_ARGV, first param is language (like 'eng'), second is record type (like 'BKS')
1216
1217     IF NEW.deleted IS TRUE THEN -- we don't much care, then, do we?
1218         RETURN NEW;
1219     END IF;
1220
1221     NEW.fingerprint := biblio.extract_fingerprint(NEW.marc);
1222     NEW.quality := biblio.extract_quality(NEW.marc, TG_ARGV[0], TG_ARGV[1]);
1223
1224     RETURN NEW;
1225
1226 END;
1227 $func$ LANGUAGE PLPGSQL;
1228
1229 CREATE OR REPLACE FUNCTION metabib.reingest_metabib_full_rec( bib_id BIGINT ) RETURNS VOID AS $func$
1230 BEGIN
1231     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
1232     IF NOT FOUND THEN
1233         DELETE FROM metabib.real_full_rec WHERE record = bib_id;
1234     END IF;
1235     INSERT INTO metabib.real_full_rec (record, tag, ind1, ind2, subfield, value)
1236         SELECT record, tag, ind1, ind2, subfield, value FROM biblio.flatten_marc( bib_id );
1237
1238     RETURN;
1239 END;
1240 $func$ LANGUAGE PLPGSQL;
1241
1242 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
1243 DECLARE
1244     uris            TEXT[];
1245     uri_xml         TEXT;
1246     uri_label       TEXT;
1247     uri_href        TEXT;
1248     uri_use         TEXT;
1249     uri_owner_list  TEXT[];
1250     uri_owner       TEXT;
1251     uri_owner_id    INT;
1252     uri_id          INT;
1253     uri_cn_id       INT;
1254     uri_map_id      INT;
1255 BEGIN
1256
1257     -- Clear any URI mappings and call numbers for this bib.
1258     -- This leads to acn / auricnm inflation, but also enables
1259     -- old acn/auricnm's to go away and for bibs to be deleted.
1260     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
1261         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
1262         DELETE FROM asset.call_number WHERE id = uri_cn_id;
1263     END LOOP;
1264
1265     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
1266     IF ARRAY_UPPER(uris,1) > 0 THEN
1267         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
1268             -- First we pull info out of the 856
1269             uri_xml     := uris[i];
1270
1271             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
1272             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()',uri_xml))[1];
1273             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
1274
1275             IF uri_label IS NULL THEN
1276                 uri_label := uri_href;
1277             END IF;
1278             CONTINUE WHEN uri_href IS NULL;
1279
1280             -- Get the distinct list of libraries wanting to use 
1281             SELECT  ARRAY_AGG(
1282                         DISTINCT REGEXP_REPLACE(
1283                             x,
1284                             $re$^.*?\((\w+)\).*$$re$,
1285                             E'\\1'
1286                         )
1287                     ) INTO uri_owner_list
1288               FROM  UNNEST(
1289                         oils_xpath(
1290                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
1291                             uri_xml
1292                         )
1293                     )x;
1294
1295             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
1296
1297                 -- look for a matching uri
1298                 IF uri_use IS NULL THEN
1299                     SELECT id INTO uri_id
1300                         FROM asset.uri
1301                         WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active
1302                         ORDER BY id LIMIT 1;
1303                     IF NOT FOUND THEN -- create one
1304                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
1305                         SELECT id INTO uri_id
1306                             FROM asset.uri
1307                             WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
1308                     END IF;
1309                 ELSE
1310                     SELECT id INTO uri_id
1311                         FROM asset.uri
1312                         WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active
1313                         ORDER BY id LIMIT 1;
1314                     IF NOT FOUND THEN -- create one
1315                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
1316                         SELECT id INTO uri_id
1317                             FROM asset.uri
1318                             WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
1319                     END IF;
1320                 END IF;
1321
1322                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
1323                     uri_owner := uri_owner_list[j];
1324
1325                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
1326                     CONTINUE WHEN NOT FOUND;
1327
1328                     -- we need a call number to link through
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                     IF NOT FOUND THEN
1331                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
1332                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
1333                         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;
1334                     END IF;
1335
1336                     -- now, link them if they're not already
1337                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
1338                     IF NOT FOUND THEN
1339                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
1340                     END IF;
1341
1342                 END LOOP;
1343
1344             END IF;
1345
1346         END LOOP;
1347     END IF;
1348
1349     RETURN;
1350 END;
1351 $func$ LANGUAGE PLPGSQL;
1352
1353 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$
1354 DECLARE
1355     new_mapping     BOOL := TRUE;
1356     source_count    INT;
1357     old_mr          BIGINT;
1358     tmp_mr          metabib.metarecord%ROWTYPE;
1359     deleted_mrs     BIGINT[];
1360 BEGIN
1361
1362     -- We need to make sure we're not a deleted master record of an MR
1363     IF bib_is_deleted THEN
1364         FOR old_mr IN SELECT id FROM metabib.metarecord WHERE master_record = bib_id LOOP
1365
1366             IF NOT retain_deleted THEN -- Go away for any MR that we're master of, unless retained
1367                 DELETE FROM metabib.metarecord_source_map WHERE source = bib_id;
1368             END IF;
1369
1370             -- Now, are there any more sources on this MR?
1371             SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = old_mr;
1372
1373             IF source_count = 0 AND NOT retain_deleted THEN -- No other records
1374                 deleted_mrs := ARRAY_APPEND(deleted_mrs, old_mr); -- Just in case...
1375                 DELETE FROM metabib.metarecord WHERE id = old_mr;
1376
1377             ELSE -- indeed there are. Update it with a null cache and recalcualated master record
1378                 UPDATE  metabib.metarecord
1379                   SET   mods = NULL,
1380                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
1381                   WHERE id = old_mr;
1382             END IF;
1383         END LOOP;
1384
1385     ELSE -- insert or update
1386
1387         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
1388
1389             -- Find the first fingerprint-matching
1390             IF old_mr IS NULL AND fp = tmp_mr.fingerprint THEN
1391                 old_mr := tmp_mr.id;
1392                 new_mapping := FALSE;
1393
1394             ELSE -- Our fingerprint changed ... maybe remove the old MR
1395                 DELETE FROM metabib.metarecord_source_map WHERE metarecord = old_mr AND source = bib_id; -- remove the old source mapping
1396                 SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id;
1397                 IF source_count = 0 THEN -- No other records
1398                     deleted_mrs := ARRAY_APPEND(deleted_mrs, tmp_mr.id);
1399                     DELETE FROM metabib.metarecord WHERE id = tmp_mr.id;
1400                 END IF;
1401             END IF;
1402
1403         END LOOP;
1404
1405         -- we found no suitable, preexisting MR based on old source maps
1406         IF old_mr IS NULL THEN
1407             SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp; -- is there one for our current fingerprint?
1408
1409             IF old_mr IS NULL THEN -- nope, create one and grab its id
1410                 INSERT INTO metabib.metarecord ( fingerprint, master_record ) VALUES ( fp, bib_id );
1411                 SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp;
1412
1413             ELSE -- indeed there is. update it with a null cache and recalcualated master record
1414                 UPDATE  metabib.metarecord
1415                   SET   mods = NULL,
1416                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
1417                   WHERE id = old_mr;
1418             END IF;
1419
1420         ELSE -- there was one we already attached to, update its mods cache and master_record
1421             UPDATE  metabib.metarecord
1422               SET   mods = NULL,
1423                     master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
1424               WHERE id = old_mr;
1425         END IF;
1426
1427         IF new_mapping THEN
1428             INSERT INTO metabib.metarecord_source_map (metarecord, source) VALUES (old_mr, bib_id); -- new source mapping
1429         END IF;
1430
1431     END IF;
1432
1433     IF ARRAY_UPPER(deleted_mrs,1) > 0 THEN
1434         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
1435     END IF;
1436
1437     RETURN old_mr;
1438
1439 END;
1440 $func$ LANGUAGE PLPGSQL;
1441
1442
1443 CREATE OR REPLACE FUNCTION biblio.map_authority_linking (bibid BIGINT, marc TEXT) RETURNS BIGINT AS $func$
1444     DELETE FROM authority.bib_linking WHERE bib = $1;
1445     INSERT INTO authority.bib_linking (bib, authority)
1446         SELECT  y.bib,
1447                 y.authority
1448           FROM (    SELECT  DISTINCT $1 AS bib,
1449                             BTRIM(remove_paren_substring(txt))::BIGINT AS authority
1450                       FROM  unnest(oils_xpath('//*[@code="0"]/text()',$2)) x(txt)
1451                       WHERE BTRIM(remove_paren_substring(txt)) ~ $re$^\d+$$re$
1452                 ) y JOIN authority.record_entry r ON r.id = y.authority;
1453     SELECT $1;
1454 $func$ LANGUAGE SQL;
1455
1456 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$
1457 DECLARE
1458     transformed_xml TEXT;
1459     rmarc           TEXT := prmarc;
1460     tmp_val         TEXT;
1461     prev_xfrm       TEXT;
1462     normalizer      RECORD;
1463     xfrm            config.xml_transform%ROWTYPE;
1464     attr_vector     INT[] := '{}'::INT[];
1465     attr_vector_tmp INT[];
1466     attr_list       TEXT[] := pattr_list;
1467     attr_value      TEXT[];
1468     norm_attr_value TEXT[];
1469     tmp_xml         TEXT;
1470     attr_def        config.record_attr_definition%ROWTYPE;
1471     ccvm_row        config.coded_value_map%ROWTYPE;
1472 BEGIN
1473
1474     IF attr_list IS NULL OR rdeleted THEN -- need to do the full dance on INSERT or undelete
1475         SELECT ARRAY_AGG(name) INTO attr_list FROM config.record_attr_definition;
1476     END IF;
1477
1478     IF rmarc IS NULL THEN
1479         SELECT marc INTO rmarc FROM biblio.record_entry WHERE id = rid;
1480     END IF;
1481
1482     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE NOT composite AND name = ANY( attr_list ) ORDER BY format LOOP
1483
1484         attr_value := '{}'::TEXT[];
1485         norm_attr_value := '{}'::TEXT[];
1486         attr_vector_tmp := '{}'::INT[];
1487
1488         SELECT * INTO ccvm_row FROM config.coded_value_map c WHERE c.ctype = attr_def.name LIMIT 1; 
1489
1490         -- tag+sf attrs only support SVF
1491         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1492             SELECT  ARRAY[ARRAY_TO_STRING(ARRAY_AGG(value), COALESCE(attr_def.joiner,' '))] INTO attr_value
1493               FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
1494               WHERE record = rid
1495                     AND tag LIKE attr_def.tag
1496                     AND CASE
1497                         WHEN attr_def.sf_list IS NOT NULL 
1498                             THEN POSITION(subfield IN attr_def.sf_list) > 0
1499                         ELSE TRUE
1500                     END
1501               GROUP BY tag
1502               ORDER BY tag
1503               LIMIT 1;
1504
1505         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1506             attr_value := vandelay.marc21_extract_fixed_field_list(rmarc, attr_def.fixed_field);
1507
1508             IF NOT attr_def.multi THEN
1509                 attr_value := ARRAY[attr_value[1]];
1510             END IF;
1511
1512         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1513
1514             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1515         
1516             -- See if we can skip the XSLT ... it's expensive
1517             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1518                 -- Can't skip the transform
1519                 IF xfrm.xslt <> '---' THEN
1520                     transformed_xml := oils_xslt_process(rmarc,xfrm.xslt);
1521                 ELSE
1522                     transformed_xml := rmarc;
1523                 END IF;
1524     
1525                 prev_xfrm := xfrm.name;
1526             END IF;
1527
1528             IF xfrm.name IS NULL THEN
1529                 -- just grab the marcxml (empty) transform
1530                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1531                 prev_xfrm := xfrm.name;
1532             END IF;
1533
1534             FOR tmp_xml IN SELECT oils_xpath(attr_def.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]) LOOP
1535                 tmp_val := oils_xpath_string(
1536                                 '//*',
1537                                 tmp_xml,
1538                                 COALESCE(attr_def.joiner,' '),
1539                                 ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
1540                             );
1541                 IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
1542                     attr_value := attr_value || tmp_val;
1543                     EXIT WHEN NOT attr_def.multi;
1544                 END IF;
1545             END LOOP;
1546
1547         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1548             SELECT  ARRAY_AGG(m.value) INTO attr_value
1549               FROM  vandelay.marc21_physical_characteristics(rmarc) v
1550                     LEFT JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
1551               WHERE v.subfield = attr_def.phys_char_sf AND (m.value IS NOT NULL AND BTRIM(m.value) <> '')
1552                     AND ( ccvm_row.id IS NULL OR ( ccvm_row.id IS NOT NULL AND v.id IS NOT NULL) );
1553
1554             IF NOT attr_def.multi THEN
1555                 attr_value := ARRAY[attr_value[1]];
1556             END IF;
1557
1558         END IF;
1559
1560                 -- apply index normalizers to attr_value
1561         FOR tmp_val IN SELECT value FROM UNNEST(attr_value) x(value) LOOP
1562             FOR normalizer IN
1563                 SELECT  n.func AS func,
1564                         n.param_count AS param_count,
1565                         m.params AS params
1566                   FROM  config.index_normalizer n
1567                         JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1568                   WHERE attr = attr_def.name
1569                   ORDER BY m.pos LOOP
1570                     EXECUTE 'SELECT ' || normalizer.func || '(' ||
1571                     COALESCE( quote_literal( tmp_val ), 'NULL' ) ||
1572                         CASE
1573                             WHEN normalizer.param_count > 0
1574                                 THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1575                                 ELSE ''
1576                             END ||
1577                     ')' INTO tmp_val;
1578
1579             END LOOP;
1580             IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
1581                 norm_attr_value := norm_attr_value || tmp_val;
1582             END IF;
1583         END LOOP;
1584         
1585         IF attr_def.filter THEN
1586             -- Create unknown uncontrolled values and find the IDs of the values
1587             IF ccvm_row.id IS NULL THEN
1588                 FOR tmp_val IN SELECT value FROM UNNEST(norm_attr_value) x(value) LOOP
1589                     IF tmp_val IS NOT NULL AND BTRIM(tmp_val) <> '' THEN
1590                         BEGIN -- use subtransaction to isolate unique constraint violations
1591                             INSERT INTO metabib.uncontrolled_record_attr_value ( attr, value ) VALUES ( attr_def.name, tmp_val );
1592                         EXCEPTION WHEN unique_violation THEN END;
1593                     END IF;
1594                 END LOOP;
1595
1596                 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 );
1597             ELSE
1598                 SELECT ARRAY_AGG(id) INTO attr_vector_tmp FROM config.coded_value_map WHERE ctype = attr_def.name AND code = ANY( norm_attr_value );
1599             END IF;
1600
1601             -- Add the new value to the vector
1602             attr_vector := attr_vector || attr_vector_tmp;
1603         END IF;
1604
1605         IF attr_def.sorter AND norm_attr_value[1] IS NOT NULL THEN
1606             DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
1607             INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, norm_attr_value[1]);
1608         END IF;
1609
1610     END LOOP;
1611
1612 /* We may need to rewrite the vlist to contain
1613    the intersection of new values for requested
1614    attrs and old values for ignored attrs. To
1615    do this, we take the old attr vlist and
1616    subtract any values that are valid for the
1617    requested attrs, and then add back the new
1618    set of attr values. */
1619
1620     IF ARRAY_LENGTH(pattr_list, 1) > 0 THEN 
1621         SELECT vlist INTO attr_vector_tmp FROM metabib.record_attr_vector_list WHERE source = rid;
1622         SELECT attr_vector_tmp - ARRAY_AGG(id::INT) INTO attr_vector_tmp FROM metabib.full_attr_id_map WHERE attr = ANY (pattr_list);
1623         attr_vector := attr_vector || attr_vector_tmp;
1624     END IF;
1625
1626     -- On to composite attributes, now that the record attrs have been pulled.  Processed in name order, so later composite
1627     -- attributes can depend on earlier ones.
1628     PERFORM metabib.compile_composite_attr_cache_init();
1629     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE composite AND name = ANY( attr_list ) ORDER BY name LOOP
1630
1631         FOR ccvm_row IN SELECT * FROM config.coded_value_map c WHERE c.ctype = attr_def.name ORDER BY value LOOP
1632
1633             tmp_val := metabib.compile_composite_attr( ccvm_row.id );
1634             CONTINUE WHEN tmp_val IS NULL OR tmp_val = ''; -- nothing to do
1635
1636             IF attr_def.filter THEN
1637                 IF attr_vector @@ tmp_val::query_int THEN
1638                     attr_vector = attr_vector + intset(ccvm_row.id);
1639                     EXIT WHEN NOT attr_def.multi;
1640                 END IF;
1641             END IF;
1642
1643             IF attr_def.sorter THEN
1644                 IF attr_vector @@ tmp_val THEN
1645                     DELETE FROM metabib.record_sorter WHERE source = rid AND attr = attr_def.name;
1646                     INSERT INTO metabib.record_sorter (source, attr, value) VALUES (rid, attr_def.name, ccvm_row.code);
1647                 END IF;
1648             END IF;
1649
1650         END LOOP;
1651
1652     END LOOP;
1653
1654     IF ARRAY_LENGTH(attr_vector, 1) > 0 THEN
1655         IF rdeleted THEN -- initial insert OR revivication
1656             DELETE FROM metabib.record_attr_vector_list WHERE source = rid;
1657             INSERT INTO metabib.record_attr_vector_list (source, vlist) VALUES (rid, attr_vector);
1658         ELSE
1659             UPDATE metabib.record_attr_vector_list SET vlist = attr_vector WHERE source = rid;
1660         END IF;
1661     END IF;
1662
1663 END;
1664
1665 $func$ LANGUAGE PLPGSQL;
1666
1667
1668 -- AFTER UPDATE OR INSERT trigger for biblio.record_entry
1669 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
1670 DECLARE
1671     tmp_bool BOOL;
1672 BEGIN
1673
1674     IF NEW.deleted THEN -- If this bib is deleted
1675
1676         PERFORM * FROM config.internal_flag WHERE
1677             name = 'ingest.metarecord_mapping.preserve_on_delete' AND enabled;
1678
1679         tmp_bool := FOUND; -- Just in case this is changed by some other statement
1680
1681         PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint, TRUE, tmp_bool );
1682
1683         IF NOT tmp_bool THEN
1684             -- One needs to keep these around to support searches
1685             -- with the #deleted modifier, so one should turn on the named
1686             -- internal flag for that functionality.
1687             DELETE FROM metabib.record_attr_vector_list WHERE source = NEW.id;
1688         END IF;
1689
1690         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
1691         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
1692         DELETE FROM metabib.browse_entry_def_map WHERE source = NEW.id; -- Don't auto-suggest deleted bibs
1693         RETURN NEW; -- and we're done
1694     END IF;
1695
1696     IF TG_OP = 'UPDATE' THEN -- re-ingest?
1697         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
1698
1699         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
1700             RETURN NEW;
1701         END IF;
1702     END IF;
1703
1704     -- Record authority linking
1705     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
1706     IF NOT FOUND THEN
1707         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
1708     END IF;
1709
1710     -- Flatten and insert the mfr data
1711     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
1712     IF NOT FOUND THEN
1713         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
1714
1715         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
1716         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
1717         IF NOT FOUND THEN
1718             PERFORM metabib.reingest_record_attributes(NEW.id, NULL, NEW.marc, TG_OP = 'INSERT' OR OLD.deleted);
1719         END IF;
1720     END IF;
1721
1722     -- Gather and insert the field entry data
1723     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
1724
1725     -- Located URI magic
1726     IF TG_OP = 'INSERT' THEN
1727         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1728         IF NOT FOUND THEN
1729             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1730         END IF;
1731     ELSE
1732         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1733         IF NOT FOUND THEN
1734             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1735         END IF;
1736     END IF;
1737
1738     -- (re)map metarecord-bib linking
1739     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
1740         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
1741         IF NOT FOUND THEN
1742             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1743         END IF;
1744     ELSE -- we're doing an update, and we're not deleted, remap
1745         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
1746         IF NOT FOUND THEN
1747             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1748         END IF;
1749     END IF;
1750
1751     RETURN NEW;
1752 END;
1753 $func$ LANGUAGE PLPGSQL;
1754
1755 CREATE OR REPLACE FUNCTION metabib.browse_normalize(facet_text TEXT, mapped_field INT) RETURNS TEXT AS $$
1756 DECLARE
1757     normalizer  RECORD;
1758 BEGIN
1759
1760     FOR normalizer IN
1761         SELECT  n.func AS func,
1762                 n.param_count AS param_count,
1763                 m.params AS params
1764           FROM  config.index_normalizer n
1765                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
1766           WHERE m.field = mapped_field AND m.pos < 0
1767           ORDER BY m.pos LOOP
1768
1769             EXECUTE 'SELECT ' || normalizer.func || '(' ||
1770                 quote_literal( facet_text ) ||
1771                 CASE
1772                     WHEN normalizer.param_count > 0
1773                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1774                         ELSE ''
1775                     END ||
1776                 ')' INTO facet_text;
1777
1778     END LOOP;
1779
1780     RETURN facet_text;
1781 END;
1782
1783 $$ LANGUAGE PLPGSQL;
1784
1785
1786 -- This mimics a specific part of QueryParser, turning the first part of a
1787 -- classed search (search_class) into a set of classes and possibly fields.
1788 -- search_class might look like "author" or "title|proper" or "ti|uniform"
1789 -- or "au" or "au|corporate|personal" or anything like that, where the first
1790 -- element of the list you get by separating on the "|" character is either
1791 -- a registered class (config.metabib_class) or an alias
1792 -- (config.metabib_search_alias), and the rest of any such elements are
1793 -- fields (config.metabib_field).
1794 CREATE OR REPLACE
1795     FUNCTION metabib.search_class_to_registered_components(search_class TEXT)
1796     RETURNS SETOF RECORD AS $func$
1797 DECLARE
1798     search_parts        TEXT[];
1799     field_name          TEXT;
1800     search_part_count   INTEGER;
1801     rec                 RECORD;
1802     registered_class    config.metabib_class%ROWTYPE;
1803     registered_alias    config.metabib_search_alias%ROWTYPE;
1804     registered_field    config.metabib_field%ROWTYPE;
1805 BEGIN
1806     search_parts := REGEXP_SPLIT_TO_ARRAY(search_class, E'\\|');
1807
1808     search_part_count := ARRAY_LENGTH(search_parts, 1);
1809     IF search_part_count = 0 THEN
1810         RETURN;
1811     ELSE
1812         SELECT INTO registered_class
1813             * FROM config.metabib_class WHERE name = search_parts[1];
1814         IF FOUND THEN
1815             IF search_part_count < 2 THEN   -- all fields
1816                 rec := (registered_class.name, NULL::INTEGER);
1817                 RETURN NEXT rec;
1818                 RETURN; -- done
1819             END IF;
1820             FOR field_name IN SELECT *
1821                 FROM UNNEST(search_parts[2:search_part_count]) LOOP
1822                 SELECT INTO registered_field
1823                     * FROM config.metabib_field
1824                     WHERE name = field_name AND
1825                         field_class = registered_class.name;
1826                 IF FOUND THEN
1827                     rec := (registered_class.name, registered_field.id);
1828                     RETURN NEXT rec;
1829                 END IF;
1830             END LOOP;
1831         ELSE
1832             -- maybe we have an alias?
1833             SELECT INTO registered_alias
1834                 * FROM config.metabib_search_alias WHERE alias=search_parts[1];
1835             IF NOT FOUND THEN
1836                 RETURN;
1837             ELSE
1838                 IF search_part_count < 2 THEN   -- return w/e the alias says
1839                     rec := (
1840                         registered_alias.field_class, registered_alias.field
1841                     );
1842                     RETURN NEXT rec;
1843                     RETURN; -- done
1844                 ELSE
1845                     FOR field_name IN SELECT *
1846                         FROM UNNEST(search_parts[2:search_part_count]) LOOP
1847                         SELECT INTO registered_field
1848                             * FROM config.metabib_field
1849                             WHERE name = field_name AND
1850                                 field_class = registered_alias.field_class;
1851                         IF FOUND THEN
1852                             rec := (
1853                                 registered_alias.field_class,
1854                                 registered_field.id
1855                             );
1856                             RETURN NEXT rec;
1857                         END IF;
1858                     END LOOP;
1859                 END IF;
1860             END IF;
1861         END IF;
1862     END IF;
1863 END;
1864 $func$ LANGUAGE PLPGSQL ROWS 1;
1865
1866
1867 -- Given a string such as a user might type into a search box, prepare
1868 -- two changed variants for TO_TSQUERY(). See
1869 -- http://www.postgresql.org/docs/9.0/static/textsearch-controls.html
1870 -- The first variant is normalized to match indexed documents regardless
1871 -- of diacritics.  The second variant keeps its diacritics for proper
1872 -- highlighting via TS_HEADLINE().
1873 CREATE OR REPLACE
1874     FUNCTION metabib.autosuggest_prepare_tsquery(orig TEXT) RETURNS TEXT[] AS
1875 $$
1876 DECLARE
1877     orig_ended_in_space     BOOLEAN;
1878     result                  RECORD;
1879     plain                   TEXT;
1880     normalized              TEXT;
1881 BEGIN
1882     orig_ended_in_space := orig ~ E'\\s$';
1883
1884     orig := ARRAY_TO_STRING(
1885         evergreen.regexp_split_to_array(orig, E'\\W+'), ' '
1886     );
1887
1888     normalized := public.naco_normalize(orig); -- also trim()s
1889     plain := trim(orig);
1890
1891     IF NOT orig_ended_in_space THEN
1892         plain := plain || ':*';
1893         normalized := normalized || ':*';
1894     END IF;
1895
1896     plain := ARRAY_TO_STRING(
1897         evergreen.regexp_split_to_array(plain, E'\\s+'), ' & '
1898     );
1899     normalized := ARRAY_TO_STRING(
1900         evergreen.regexp_split_to_array(normalized, E'\\s+'), ' & '
1901     );
1902
1903     RETURN ARRAY[normalized, plain];
1904 END;
1905 $$ LANGUAGE PLPGSQL;
1906
1907
1908 CREATE OR REPLACE
1909     FUNCTION metabib.suggest_browse_entries(
1910         raw_query_text  TEXT,   -- actually typed by humans at the UI level
1911         search_class    TEXT,   -- 'alias' or 'class' or 'class|field..', etc
1912         headline_opts   TEXT,   -- markup options for ts_headline()
1913         visibility_org  INTEGER,-- null if you don't want opac visibility test
1914         query_limit     INTEGER,-- use in LIMIT clause of interal query
1915         normalization   INTEGER -- argument to TS_RANK_CD()
1916     ) RETURNS TABLE (
1917         value                   TEXT,   -- plain
1918         field                   INTEGER,
1919         buoyant_and_class_match BOOL,
1920         field_match             BOOL,
1921         field_weight            INTEGER,
1922         rank                    REAL,
1923         buoyant                 BOOL,
1924         match                   TEXT    -- marked up
1925     ) AS $func$
1926 DECLARE
1927     prepared_query_texts    TEXT[];
1928     query                   TSQUERY;
1929     plain_query             TSQUERY;
1930     opac_visibility_join    TEXT;
1931     search_class_join       TEXT;
1932     r_fields                RECORD;
1933 BEGIN
1934     prepared_query_texts := metabib.autosuggest_prepare_tsquery(raw_query_text);
1935
1936     query := TO_TSQUERY('keyword', prepared_query_texts[1]);
1937     plain_query := TO_TSQUERY('keyword', prepared_query_texts[2]);
1938
1939     visibility_org := NULLIF(visibility_org,-1);
1940     IF visibility_org IS NOT NULL THEN
1941         opac_visibility_join := '
1942     JOIN asset.opac_visible_copies aovc ON (
1943         aovc.record = x.source AND
1944         aovc.circ_lib IN (SELECT id FROM actor.org_unit_descendants($4))
1945     )';
1946     ELSE
1947         opac_visibility_join := '';
1948     END IF;
1949
1950     -- The following determines whether we only provide suggestsons matching
1951     -- the user's selected search_class, or whether we show other suggestions
1952     -- too. The reason for MIN() is that for search_classes like
1953     -- 'title|proper|uniform' you would otherwise get multiple rows.  The
1954     -- implication is that if title as a class doesn't have restrict,
1955     -- nor does the proper field, but the uniform field does, you're going
1956     -- to get 'false' for your overall evaluation of 'should we restrict?'
1957     -- To invert that, change from MIN() to MAX().
1958
1959     SELECT
1960         INTO r_fields
1961             MIN(cmc.restrict::INT) AS restrict_class,
1962             MIN(cmf.restrict::INT) AS restrict_field
1963         FROM metabib.search_class_to_registered_components(search_class)
1964             AS _registered (field_class TEXT, field INT)
1965         JOIN
1966             config.metabib_class cmc ON (cmc.name = _registered.field_class)
1967         LEFT JOIN
1968             config.metabib_field cmf ON (cmf.id = _registered.field);
1969
1970     -- evaluate 'should we restrict?'
1971     IF r_fields.restrict_field::BOOL OR r_fields.restrict_class::BOOL THEN
1972         search_class_join := '
1973     JOIN
1974         metabib.search_class_to_registered_components($2)
1975         AS _registered (field_class TEXT, field INT) ON (
1976             (_registered.field IS NULL AND
1977                 _registered.field_class = cmf.field_class) OR
1978             (_registered.field = cmf.id)
1979         )
1980     ';
1981     ELSE
1982         search_class_join := '
1983     LEFT JOIN
1984         metabib.search_class_to_registered_components($2)
1985         AS _registered (field_class TEXT, field INT) ON (
1986             _registered.field_class = cmc.name
1987         )
1988     ';
1989     END IF;
1990
1991     RETURN QUERY EXECUTE '
1992 SELECT  DISTINCT
1993         x.value,
1994         x.id,
1995         x.push,
1996         x.restrict,
1997         x.weight,
1998         x.ts_rank_cd,
1999         x.buoyant,
2000         TS_HEADLINE(value, $7, $3)
2001   FROM  (SELECT DISTINCT
2002                 mbe.value,
2003                 cmf.id,
2004                 cmc.buoyant AND _registered.field_class IS NOT NULL AS push,
2005                 _registered.field = cmf.id AS restrict,
2006                 cmf.weight,
2007                 TS_RANK_CD(mbe.index_vector, $1, $6),
2008                 cmc.buoyant,
2009                 mbedm.source
2010           FROM  metabib.browse_entry_def_map mbedm
2011                 JOIN (SELECT * FROM metabib.browse_entry WHERE index_vector @@ $1 LIMIT 10000) mbe ON (mbe.id = mbedm.entry)
2012                 JOIN config.metabib_field cmf ON (cmf.id = mbedm.def)
2013                 JOIN config.metabib_class cmc ON (cmf.field_class = cmc.name)
2014                 '  || search_class_join || '
2015           ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
2016           LIMIT 1000) AS x
2017         ' || opac_visibility_join || '
2018   ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
2019   LIMIT $5
2020 '   -- sic, repeat the order by clause in the outer select too
2021     USING
2022         query, search_class, headline_opts,
2023         visibility_org, query_limit, normalization, plain_query
2024         ;
2025
2026     -- sort order:
2027     --  buoyant AND chosen class = match class
2028     --  chosen field = match field
2029     --  field weight
2030     --  rank
2031     --  buoyancy
2032     --  value itself
2033
2034 END;
2035 $func$ LANGUAGE PLPGSQL;
2036
2037 CREATE OR REPLACE FUNCTION public.oils_tsearch2 () RETURNS TRIGGER AS $$
2038 DECLARE
2039     normalizer      RECORD;
2040     value           TEXT := '';
2041     temp_vector     TEXT := '';
2042     ts_rec          RECORD;
2043     cur_weight      "char";
2044 BEGIN
2045
2046     value := NEW.value;
2047     NEW.index_vector = ''::tsvector;
2048
2049     IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
2050         FOR normalizer IN
2051             SELECT  n.func AS func,
2052                     n.param_count AS param_count,
2053                     m.params AS params
2054               FROM  config.index_normalizer n
2055                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
2056               WHERE field = NEW.field AND m.pos < 0
2057               ORDER BY m.pos LOOP
2058                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
2059                     quote_literal( value ) ||
2060                     CASE
2061                         WHEN normalizer.param_count > 0
2062                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
2063                             ELSE ''
2064                         END ||
2065                     ')' INTO value;
2066
2067         END LOOP;
2068
2069         NEW.value = value;
2070
2071         FOR normalizer IN
2072             SELECT  n.func AS func,
2073                     n.param_count AS param_count,
2074                     m.params AS params
2075               FROM  config.index_normalizer n
2076                     JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
2077               WHERE field = NEW.field AND m.pos >= 0
2078               ORDER BY m.pos LOOP
2079                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
2080                     quote_literal( value ) ||
2081                     CASE
2082                         WHEN normalizer.param_count > 0
2083                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
2084                             ELSE ''
2085                         END ||
2086                     ')' INTO value;
2087
2088         END LOOP;
2089    END IF;
2090
2091     IF TG_TABLE_NAME::TEXT ~ 'browse_entry$' THEN
2092
2093         value :=  ARRAY_TO_STRING(
2094             evergreen.regexp_split_to_array(value, E'\\W+'), ' '
2095         );
2096         value := public.search_normalize(value);
2097         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
2098
2099     ELSIF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
2100         FOR ts_rec IN
2101
2102             SELECT DISTINCT m.ts_config, m.index_weight
2103             FROM config.metabib_class_ts_map m
2104                  LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
2105                  LEFT JOIN config.coded_value_map ccvm ON (
2106                     ccvm.ctype IN ('item_lang', 'language') AND
2107                     ccvm.code = m.index_lang AND
2108                     r.vlist @> intset(ccvm.id)
2109                 )
2110             WHERE m.field_class = TG_ARGV[0]
2111                 AND m.active
2112                 AND (m.always OR NOT EXISTS (SELECT 1 FROM config.metabib_field_ts_map WHERE metabib_field = NEW.field))
2113                 AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
2114                         UNION
2115             SELECT DISTINCT m.ts_config, m.index_weight
2116             FROM config.metabib_field_ts_map m
2117                  LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
2118                  LEFT JOIN config.coded_value_map ccvm ON (
2119                     ccvm.ctype IN ('item_lang', 'language') AND
2120                     ccvm.code = m.index_lang AND
2121                     r.vlist @> intset(ccvm.id)
2122                 )
2123             WHERE m.metabib_field = NEW.field
2124                 AND m.active
2125                 AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
2126             ORDER BY index_weight ASC
2127
2128         LOOP
2129
2130             IF cur_weight IS NOT NULL AND cur_weight != ts_rec.index_weight THEN
2131                 NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
2132                 temp_vector = '';
2133             END IF;
2134
2135             cur_weight = ts_rec.index_weight;
2136             SELECT INTO temp_vector temp_vector || ' ' || to_tsvector(ts_rec.ts_config::regconfig, value)::TEXT;
2137
2138         END LOOP;
2139         NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
2140     ELSE
2141         NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
2142     END IF;
2143
2144     RETURN NEW;
2145 END;
2146 $$ LANGUAGE PLPGSQL;
2147
2148
2149 CREATE TYPE metabib.flat_browse_entry_appearance AS (
2150     browse_entry    BIGINT,
2151     value           TEXT,
2152     fields          TEXT,
2153     authorities     TEXT,
2154     sees            TEXT,
2155     sources         INT,        -- visible ones, that is
2156     asources        INT,        -- visible ones, that is
2157     row_number      INT,        -- internal use, sort of
2158     accurate        BOOL,       -- Count in sources field is accurate? Not
2159                                 -- if we had more than a browse superpage
2160                                 -- of records to look at.
2161     aaccurate       BOOL,       -- See previous comment...
2162     pivot_point     BIGINT
2163 );
2164
2165
2166 CREATE OR REPLACE FUNCTION metabib.browse_bib_pivot(
2167     INT[],
2168     TEXT
2169 ) RETURNS BIGINT AS $p$
2170     SELECT  mbe.id
2171       FROM  metabib.browse_entry mbe
2172             JOIN metabib.browse_entry_def_map mbedm ON (
2173                 mbedm.entry = mbe.id
2174                 AND mbedm.def = ANY($1)
2175             )
2176       WHERE mbe.sort_value >= public.naco_normalize($2)
2177       ORDER BY mbe.sort_value, mbe.value LIMIT 1;
2178 $p$ LANGUAGE SQL STABLE;
2179
2180 CREATE OR REPLACE FUNCTION metabib.browse_authority_pivot(
2181     INT[],
2182     TEXT
2183 ) RETURNS BIGINT AS $p$
2184     SELECT  mbe.id
2185       FROM  metabib.browse_entry mbe
2186             JOIN metabib.browse_entry_simple_heading_map mbeshm ON ( mbeshm.entry = mbe.id )
2187             JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2188             JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2189                 ash.atag = map.authority_field
2190                 AND map.metabib_field = ANY($1)
2191             )
2192       WHERE mbe.sort_value >= public.naco_normalize($2)
2193       ORDER BY mbe.sort_value, mbe.value LIMIT 1;
2194 $p$ LANGUAGE SQL STABLE;
2195
2196 CREATE OR REPLACE FUNCTION metabib.browse_authority_refs_pivot(
2197     INT[],
2198     TEXT
2199 ) RETURNS BIGINT AS $p$
2200     SELECT  mbe.id
2201       FROM  metabib.browse_entry mbe
2202             JOIN metabib.browse_entry_simple_heading_map mbeshm ON ( mbeshm.entry = mbe.id )
2203             JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2204             JOIN authority.control_set_auth_field_metabib_field_map_refs_only map ON (
2205                 ash.atag = map.authority_field
2206                 AND map.metabib_field = ANY($1)
2207             )
2208       WHERE mbe.sort_value >= public.naco_normalize($2)
2209       ORDER BY mbe.sort_value, mbe.value LIMIT 1;
2210 $p$ LANGUAGE SQL STABLE;
2211
2212 CREATE OR REPLACE FUNCTION metabib.browse_pivot(
2213     INT[],
2214     TEXT
2215 ) RETURNS BIGINT AS $p$
2216     SELECT  id FROM metabib.browse_entry
2217       WHERE id IN (
2218                 metabib.browse_bib_pivot($1, $2),
2219                 metabib.browse_authority_refs_pivot($1,$2) -- only look in 4xx, 5xx, 7xx of authority
2220             )
2221       ORDER BY sort_value, value LIMIT 1;
2222 $p$ LANGUAGE SQL STABLE;
2223
2224
2225 CREATE OR REPLACE FUNCTION metabib.staged_browse(
2226     query                   TEXT,
2227     fields                  INT[],
2228     context_org             INT,
2229     context_locations       INT[],
2230     staff                   BOOL,
2231     browse_superpage_size   INT,
2232     count_up_from_zero      BOOL,   -- if false, count down from -1
2233     result_limit            INT,
2234     next_pivot_pos          INT
2235 ) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
2236 DECLARE
2237     curs                    REFCURSOR;
2238     rec                     RECORD;
2239     qpfts_query             TEXT;
2240     aqpfts_query            TEXT;
2241     afields                 INT[];
2242     bfields                 INT[];
2243     result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
2244     results_skipped         INT := 0;
2245     row_counter             INT := 0;
2246     row_number              INT;
2247     slice_start             INT;
2248     slice_end               INT;
2249     full_end                INT;
2250     all_records             BIGINT[];
2251     all_brecords             BIGINT[];
2252     all_arecords            BIGINT[];
2253     superpage_of_records    BIGINT[];
2254     superpage_size          INT;
2255 BEGIN
2256     IF count_up_from_zero THEN
2257         row_number := 0;
2258     ELSE
2259         row_number := -1;
2260     END IF;
2261
2262     OPEN curs FOR EXECUTE query;
2263
2264     LOOP
2265         FETCH curs INTO rec;
2266         IF NOT FOUND THEN
2267             IF result_row.pivot_point IS NOT NULL THEN
2268                 RETURN NEXT result_row;
2269             END IF;
2270             RETURN;
2271         END IF;
2272
2273
2274         -- Gather aggregate data based on the MBE row we're looking at now, authority axis
2275         SELECT INTO all_arecords, result_row.sees, afields
2276                 ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
2277                 STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
2278                 ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
2279
2280           FROM  metabib.browse_entry_simple_heading_map mbeshm
2281                 JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2282                 JOIN authority.authority_linking aal ON ( ash.record = aal.source )
2283                 JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
2284                 JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2285                     ash.atag = map.authority_field
2286                     AND map.metabib_field = ANY(fields)
2287                 )
2288           WHERE mbeshm.entry = rec.id;
2289
2290
2291         -- Gather aggregate data based on the MBE row we're looking at now, bib axis
2292         SELECT INTO all_brecords, result_row.authorities, bfields
2293                 ARRAY_AGG(DISTINCT source),
2294                 STRING_AGG(DISTINCT authority::TEXT, $$,$$),
2295                 ARRAY_AGG(DISTINCT def)
2296           FROM  metabib.browse_entry_def_map
2297           WHERE entry = rec.id
2298                 AND def = ANY(fields);
2299
2300         SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
2301
2302         result_row.sources := 0;
2303         result_row.asources := 0;
2304
2305         -- Bib-linked vis checking
2306         IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
2307
2308             full_end := ARRAY_LENGTH(all_brecords, 1);
2309             superpage_size := COALESCE(browse_superpage_size, full_end);
2310             slice_start := 1;
2311             slice_end := superpage_size;
2312
2313             WHILE result_row.sources = 0 AND slice_start <= full_end LOOP
2314                 superpage_of_records := all_brecords[slice_start:slice_end];
2315                 qpfts_query :=
2316                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
2317                     '1::INT AS rel FROM (SELECT UNNEST(' ||
2318                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
2319
2320                 -- We use search.query_parser_fts() for visibility testing.
2321                 -- We're calling it once per browse-superpage worth of records
2322                 -- out of the set of records related to a given mbe, until we've
2323                 -- either exhausted that set of records or found at least 1
2324                 -- visible record.
2325
2326                 SELECT INTO result_row.sources visible
2327                     FROM search.query_parser_fts(
2328                         context_org, NULL, qpfts_query, NULL,
2329                         context_locations, 0, NULL, NULL, FALSE, staff, FALSE
2330                     ) qpfts
2331                     WHERE qpfts.rel IS NULL;
2332
2333                 slice_start := slice_start + superpage_size;
2334                 slice_end := slice_end + superpage_size;
2335             END LOOP;
2336
2337             -- Accurate?  Well, probably.
2338             result_row.accurate := browse_superpage_size IS NULL OR
2339                 browse_superpage_size >= full_end;
2340
2341         END IF;
2342
2343         -- Authority-linked vis checking
2344         IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
2345
2346             full_end := ARRAY_LENGTH(all_arecords, 1);
2347             superpage_size := COALESCE(browse_superpage_size, full_end);
2348             slice_start := 1;
2349             slice_end := superpage_size;
2350
2351             WHILE result_row.asources = 0 AND slice_start <= full_end LOOP
2352                 superpage_of_records := all_arecords[slice_start:slice_end];
2353                 qpfts_query :=
2354                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
2355                     '1::INT AS rel FROM (SELECT UNNEST(' ||
2356                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
2357
2358                 -- We use search.query_parser_fts() for visibility testing.
2359                 -- We're calling it once per browse-superpage worth of records
2360                 -- out of the set of records related to a given mbe, via
2361                 -- authority until we've either exhausted that set of records
2362                 -- or found at least 1 visible record.
2363
2364                 SELECT INTO result_row.asources visible
2365                     FROM search.query_parser_fts(
2366                         context_org, NULL, qpfts_query, NULL,
2367                         context_locations, 0, NULL, NULL, FALSE, staff, FALSE
2368                     ) qpfts
2369                     WHERE qpfts.rel IS NULL;
2370
2371                 slice_start := slice_start + superpage_size;
2372                 slice_end := slice_end + superpage_size;
2373             END LOOP;
2374
2375
2376             -- Accurate?  Well, probably.
2377             result_row.aaccurate := browse_superpage_size IS NULL OR
2378                 browse_superpage_size >= full_end;
2379
2380         END IF;
2381
2382         IF result_row.sources > 0 OR result_row.asources > 0 THEN
2383
2384             -- The function that calls this function needs row_number in order
2385             -- to correctly order results from two different runs of this
2386             -- functions.
2387             result_row.row_number := row_number;
2388
2389             -- Now, if row_counter is still less than limit, return a row.  If
2390             -- not, but it is less than next_pivot_pos, continue on without
2391             -- returning actual result rows until we find
2392             -- that next pivot, and return it.
2393
2394             IF row_counter < result_limit THEN
2395                 result_row.browse_entry := rec.id;
2396                 result_row.value := rec.value;
2397
2398                 RETURN NEXT result_row;
2399             ELSE
2400                 result_row.browse_entry := NULL;
2401                 result_row.authorities := NULL;
2402                 result_row.fields := NULL;
2403                 result_row.value := NULL;
2404                 result_row.sources := NULL;
2405                 result_row.sees := NULL;
2406                 result_row.accurate := NULL;
2407                 result_row.aaccurate := NULL;
2408                 result_row.pivot_point := rec.id;
2409
2410                 IF row_counter >= next_pivot_pos THEN
2411                     RETURN NEXT result_row;
2412                     RETURN;
2413                 END IF;
2414             END IF;
2415
2416             IF count_up_from_zero THEN
2417                 row_number := row_number + 1;
2418             ELSE
2419                 row_number := row_number - 1;
2420             END IF;
2421
2422             -- row_counter is different from row_number.
2423             -- It simply counts up from zero so that we know when
2424             -- we've reached our limit.
2425             row_counter := row_counter + 1;
2426         END IF;
2427     END LOOP;
2428 END;
2429 $p$ LANGUAGE PLPGSQL;
2430
2431
2432 CREATE OR REPLACE FUNCTION metabib.browse(
2433     search_field            INT[],
2434     browse_term             TEXT,
2435     context_org             INT DEFAULT NULL,
2436     context_loc_group       INT DEFAULT NULL,
2437     staff                   BOOL DEFAULT FALSE,
2438     pivot_id                BIGINT DEFAULT NULL,
2439     result_limit            INT DEFAULT 10
2440 ) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
2441 DECLARE
2442     core_query              TEXT;
2443     back_query              TEXT;
2444     forward_query           TEXT;
2445     pivot_sort_value        TEXT;
2446     pivot_sort_fallback     TEXT;
2447     context_locations       INT[];
2448     browse_superpage_size   INT;
2449     results_skipped         INT := 0;
2450     back_limit              INT;
2451     back_to_pivot           INT;
2452     forward_limit           INT;
2453     forward_to_pivot        INT;
2454 BEGIN
2455     -- First, find the pivot if we were given a browse term but not a pivot.
2456     IF pivot_id IS NULL THEN
2457         pivot_id := metabib.browse_pivot(search_field, browse_term);
2458     END IF;
2459
2460     SELECT INTO pivot_sort_value, pivot_sort_fallback
2461         sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
2462
2463     -- Bail if we couldn't find a pivot.
2464     IF pivot_sort_value IS NULL THEN
2465         RETURN;
2466     END IF;
2467
2468     -- Transform the context_loc_group argument (if any) (logc at the
2469     -- TPAC layer) into a form we'll be able to use.
2470     IF context_loc_group IS NOT NULL THEN
2471         SELECT INTO context_locations ARRAY_AGG(location)
2472             FROM asset.copy_location_group_map
2473             WHERE lgroup = context_loc_group;
2474     END IF;
2475
2476     -- Get the configured size of browse superpages.
2477     SELECT INTO browse_superpage_size value     -- NULL ok
2478         FROM config.global_flag
2479         WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
2480
2481     -- First we're going to search backward from the pivot, then we're going
2482     -- to search forward.  In each direction, we need two limits.  At the
2483     -- lesser of the two limits, we delineate the edge of the result set
2484     -- we're going to return.  At the greater of the two limits, we find the
2485     -- pivot value that would represent an offset from the current pivot
2486     -- at a distance of one "page" in either direction, where a "page" is a
2487     -- result set of the size specified in the "result_limit" argument.
2488     --
2489     -- The two limits in each direction make four derived values in total,
2490     -- and we calculate them now.
2491     back_limit := CEIL(result_limit::FLOAT / 2);
2492     back_to_pivot := result_limit;
2493     forward_limit := result_limit / 2;
2494     forward_to_pivot := result_limit - 1;
2495
2496     -- This is the meat of the SQL query that finds browse entries.  We'll
2497     -- pass this to a function which uses it with a cursor, so that individual
2498     -- rows may be fetched in a loop until some condition is satisfied, without
2499     -- waiting for a result set of fixed size to be collected all at once.
2500     core_query := '
2501 SELECT  mbe.id,
2502         mbe.value,
2503         mbe.sort_value
2504   FROM  metabib.browse_entry mbe
2505   WHERE (
2506             EXISTS ( -- are there any bibs using this mbe via the requested fields?
2507                 SELECT  1
2508                   FROM  metabib.browse_entry_def_map mbedm
2509                   WHERE mbedm.entry = mbe.id AND mbedm.def = ANY(' || quote_literal(search_field) || ')
2510                   LIMIT 1
2511             ) OR EXISTS ( -- are there any authorities using this mbe via the requested fields?
2512                 SELECT  1
2513                   FROM  metabib.browse_entry_simple_heading_map mbeshm
2514                         JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2515                         JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2516                             ash.atag = map.authority_field
2517                             AND map.metabib_field = ANY(' || quote_literal(search_field) || ')
2518                         )
2519                   WHERE mbeshm.entry = mbe.id
2520             )
2521         ) AND ';
2522
2523     -- This is the variant of the query for browsing backward.
2524     back_query := core_query ||
2525         ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
2526     ' ORDER BY mbe.sort_value DESC, mbe.value DESC ';
2527
2528     -- This variant browses forward.
2529     forward_query := core_query ||
2530         ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
2531     ' ORDER BY mbe.sort_value, mbe.value ';
2532
2533     -- We now call the function which applies a cursor to the provided
2534     -- queries, stopping at the appropriate limits and also giving us
2535     -- the next page's pivot.
2536     RETURN QUERY
2537         SELECT * FROM metabib.staged_browse(
2538             back_query, search_field, context_org, context_locations,
2539             staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
2540         ) UNION
2541         SELECT * FROM metabib.staged_browse(
2542             forward_query, search_field, context_org, context_locations,
2543             staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
2544         ) ORDER BY row_number DESC;
2545
2546 END;
2547 $p$ LANGUAGE PLPGSQL;
2548
2549 CREATE OR REPLACE FUNCTION metabib.browse(
2550     search_class        TEXT,
2551     browse_term         TEXT,
2552     context_org         INT DEFAULT NULL,
2553     context_loc_group   INT DEFAULT NULL,
2554     staff               BOOL DEFAULT FALSE,
2555     pivot_id            BIGINT DEFAULT NULL,
2556     result_limit        INT DEFAULT 10
2557 ) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
2558 BEGIN
2559     RETURN QUERY SELECT * FROM metabib.browse(
2560         (SELECT COALESCE(ARRAY_AGG(id), ARRAY[]::INT[])
2561             FROM config.metabib_field WHERE field_class = search_class),
2562         browse_term,
2563         context_org,
2564         context_loc_group,
2565         staff,
2566         pivot_id,
2567         result_limit
2568     );
2569 END;
2570 $p$ LANGUAGE PLPGSQL;
2571
2572 COMMIT;
2573