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