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