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