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