LP#1744385: Search and Result Display improvements
[working/Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / XXXX.schema.highlight_search.sql
1 BEGIN;
2
3 ALTER TABLE config.metabib_field ALTER COLUMN xpath DROP NOT NULL;
4
5 CREATE TABLE config.metabib_field_virtual_map (
6     id      SERIAL  PRIMARY KEY,
7     real    INT NOT NULL REFERENCES config.metabib_field (id),
8     virtual INT NOT NULL REFERENCES config.metabib_field (id),
9     weight  INT NOT NULL DEFAULT 1
10 );
11 COMMENT ON TABLE config.metabib_field_virtual_map IS $$
12 Maps between real (physically extracted) index definitions
13 and virtual (target sync, no required extraction of its own)
14 index definitions.
15
16 The virtual side may not extract any data of its own, but
17 will collect data from all of the real fields.  This reduces
18 extraction (ingest) overhead by eliminating duplcated extraction,
19 and allows for searching across novel combinations of fields, such
20 as names used as either subjects or authors.  By preserving this
21 mapping rather than defining duplicate extractions, information
22 about the originating, "real" index definitions can be used
23 in interesting ways, such as highlighting in search results.
24 $$;
25
26 CREATE OR REPLACE VIEW metabib.combined_all_field_entry AS
27     SELECT * FROM metabib.combined_title_field_entry
28         UNION ALL
29     SELECT * FROM metabib.combined_author_field_entry
30         UNION ALL
31     SELECT * FROM metabib.combined_subject_field_entry
32         UNION ALL
33     SELECT * FROM metabib.combined_keyword_field_entry
34         UNION ALL
35     SELECT * FROM metabib.combined_identifier_field_entry
36         UNION ALL
37     SELECT * FROM metabib.combined_series_field_entry;
38
39
40 CREATE OR REPLACE FUNCTION biblio.extract_metabib_field_entry (
41     rid BIGINT,
42     default_joiner TEXT,
43     field_types TEXT[],
44     only_fields INT[]
45 ) RETURNS SETOF metabib.field_entry_template AS $func$
46 DECLARE
47     bib     biblio.record_entry%ROWTYPE;
48     idx     config.metabib_field%ROWTYPE;
49     xfrm        config.xml_transform%ROWTYPE;
50     prev_xfrm   TEXT;
51     transformed_xml TEXT;
52     xml_node    TEXT;
53     xml_node_list   TEXT[];
54     facet_text  TEXT;
55     display_text TEXT;
56     browse_text TEXT;
57     sort_value  TEXT;
58     raw_text    TEXT;
59     curr_text   TEXT;
60     joiner      TEXT := default_joiner; -- XXX will index defs supply a joiner?
61     authority_text TEXT;
62     authority_link BIGINT;
63     output_row  metabib.field_entry_template%ROWTYPE;
64     process_idx BOOL;
65 BEGIN
66
67     -- Start out with no field-use bools set
68     output_row.browse_field = FALSE;
69     output_row.facet_field = FALSE;
70     output_row.display_field = FALSE;
71     output_row.search_field = FALSE;
72
73     -- Get the record
74     SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
75
76     -- Loop over the indexing entries
77     FOR idx IN SELECT * FROM config.metabib_field WHERE id = ANY (only_fields) ORDER BY format LOOP
78         CONTINUE WHEN idx.xpath IS NULL OR idx.xpath = ''; -- pure virtual field
79
80         process_idx := FALSE;
81         IF idx.display_field AND 'display' = ANY (field_types) THEN process_idx = TRUE; END IF;
82         IF idx.browse_field AND 'browse' = ANY (field_types) THEN process_idx = TRUE; END IF;
83         IF idx.search_field AND 'search' = ANY (field_types) THEN process_idx = TRUE; END IF;
84         IF idx.facet_field AND 'facet' = ANY (field_types) THEN process_idx = TRUE; END IF;
85         CONTINUE WHEN process_idx = FALSE; -- disabled for all types
86
87         joiner := COALESCE(idx.joiner, default_joiner);
88
89         SELECT INTO xfrm * from config.xml_transform WHERE name = idx.format;
90
91         -- See if we can skip the XSLT ... it's expensive
92         IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
93             -- Can't skip the transform
94             IF xfrm.xslt <> '---' THEN
95                 transformed_xml := oils_xslt_process(bib.marc,xfrm.xslt);
96             ELSE
97                 transformed_xml := bib.marc;
98             END IF;
99
100             prev_xfrm := xfrm.name;
101         END IF;
102
103         xml_node_list := oils_xpath( idx.xpath, transformed_xml, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
104
105         raw_text := NULL;
106         FOR xml_node IN SELECT x FROM unnest(xml_node_list) AS x LOOP
107             CONTINUE WHEN xml_node !~ E'^\\s*<';
108
109             -- XXX much of this should be moved into oils_xpath_string...
110             curr_text := ARRAY_TO_STRING(evergreen.array_remove_item_by_value(evergreen.array_remove_item_by_value(
111                 oils_xpath( '//text()', -- get the content of all the nodes within the main selected node
112                     REGEXP_REPLACE( xml_node, E'\\s+', ' ', 'g' ) -- Translate adjacent whitespace to a single space
113                 ), ' '), ''),  -- throw away morally empty (bankrupt?) strings
114                 joiner
115             );
116
117             CONTINUE WHEN curr_text IS NULL OR curr_text = '';
118
119             IF raw_text IS NOT NULL THEN
120                 raw_text := raw_text || joiner;
121             END IF;
122
123             raw_text := COALESCE(raw_text,'') || curr_text;
124
125             -- autosuggest/metabib.browse_entry
126             IF idx.browse_field THEN
127
128                 IF idx.browse_xpath IS NOT NULL AND idx.browse_xpath <> '' THEN
129                     browse_text := oils_xpath_string( idx.browse_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
130                 ELSE
131                     browse_text := curr_text;
132                 END IF;
133
134                 IF idx.browse_sort_xpath IS NOT NULL AND
135                     idx.browse_sort_xpath <> '' THEN
136
137                     sort_value := oils_xpath_string(
138                         idx.browse_sort_xpath, xml_node, joiner,
139                         ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]
140                     );
141                 ELSE
142                     sort_value := browse_text;
143                 END IF;
144
145                 output_row.field_class = idx.field_class;
146                 output_row.field = idx.id;
147                 output_row.source = rid;
148                 output_row.value = BTRIM(REGEXP_REPLACE(browse_text, E'\\s+', ' ', 'g'));
149                 output_row.sort_value :=
150                     public.naco_normalize(sort_value);
151
152                 output_row.authority := NULL;
153
154                 IF idx.authority_xpath IS NOT NULL AND idx.authority_xpath <> '' THEN
155                     authority_text := oils_xpath_string(
156                         idx.authority_xpath, xml_node, joiner,
157                         ARRAY[
158                             ARRAY[xfrm.prefix, xfrm.namespace_uri],
159                             ARRAY['xlink','http://www.w3.org/1999/xlink']
160                         ]
161                     );
162
163                     IF authority_text ~ '^\d+$' THEN
164                         authority_link := authority_text::BIGINT;
165                         PERFORM * FROM authority.record_entry WHERE id = authority_link;
166                         IF FOUND THEN
167                             output_row.authority := authority_link;
168                         END IF;
169                     END IF;
170
171                 END IF;
172
173                 output_row.browse_field = TRUE;
174                 -- Returning browse rows with search_field = true for search+browse
175                 -- configs allows us to retain granularity of being able to search
176                 -- browse fields with "starts with" type operators (for example, for
177                 -- titles of songs in music albums)
178                 IF idx.search_field THEN
179                     output_row.search_field = TRUE;
180                 END IF;
181                 RETURN NEXT output_row;
182                 output_row.browse_field = FALSE;
183                 output_row.search_field = FALSE;
184                 output_row.sort_value := NULL;
185             END IF;
186
187             -- insert raw node text for faceting
188             IF idx.facet_field THEN
189
190                 IF idx.facet_xpath IS NOT NULL AND idx.facet_xpath <> '' THEN
191                     facet_text := oils_xpath_string( idx.facet_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
192                 ELSE
193                     facet_text := curr_text;
194                 END IF;
195
196                 output_row.field_class = idx.field_class;
197                 output_row.field = -1 * idx.id;
198                 output_row.source = rid;
199                 output_row.value = BTRIM(REGEXP_REPLACE(facet_text, E'\\s+', ' ', 'g'));
200
201                 output_row.facet_field = TRUE;
202                 RETURN NEXT output_row;
203                 output_row.facet_field = FALSE;
204             END IF;
205
206             -- insert raw node text for display
207             IF idx.display_field THEN
208
209                 IF idx.display_xpath IS NOT NULL AND idx.display_xpath <> '' THEN
210                     display_text := oils_xpath_string( idx.display_xpath, xml_node, joiner, ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]] );
211                 ELSE
212                     display_text := curr_text;
213                 END IF;
214
215                 output_row.field_class = idx.field_class;
216                 output_row.field = -1 * idx.id;
217                 output_row.source = rid;
218                 output_row.value = BTRIM(REGEXP_REPLACE(display_text, E'\\s+', ' ', 'g'));
219
220                 output_row.display_field = TRUE;
221                 RETURN NEXT output_row;
222                 output_row.display_field = FALSE;
223             END IF;
224
225         END LOOP;
226
227         CONTINUE WHEN raw_text IS NULL OR raw_text = '';
228
229         -- insert combined node text for searching
230         IF idx.search_field THEN
231             output_row.field_class = idx.field_class;
232             output_row.field = idx.id;
233             output_row.source = rid;
234             output_row.value = BTRIM(REGEXP_REPLACE(raw_text, E'\\s+', ' ', 'g'));
235
236             output_row.search_field = TRUE;
237             RETURN NEXT output_row;
238             output_row.search_field = FALSE;
239         END IF;
240
241     END LOOP;
242
243 END;
244 $func$ LANGUAGE PLPGSQL;
245
246 CREATE OR REPLACE FUNCTION metabib.update_combined_index_vectors(bib_id BIGINT) RETURNS VOID AS $func$
247 DECLARE
248     rdata       TSVECTOR;
249     vclass      TEXT;
250     vfield      INT;
251     rfields     INT[];
252 BEGIN
253     DELETE FROM metabib.combined_keyword_field_entry WHERE record = bib_id;
254     INSERT INTO metabib.combined_keyword_field_entry(record, metabib_field, index_vector)
255         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
256         FROM metabib.keyword_field_entry WHERE source = bib_id GROUP BY field;
257     INSERT INTO metabib.combined_keyword_field_entry(record, metabib_field, index_vector)
258         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
259         FROM metabib.keyword_field_entry WHERE source = bib_id;
260
261     DELETE FROM metabib.combined_title_field_entry WHERE record = bib_id;
262     INSERT INTO metabib.combined_title_field_entry(record, metabib_field, index_vector)
263         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
264         FROM metabib.title_field_entry WHERE source = bib_id GROUP BY field;
265     INSERT INTO metabib.combined_title_field_entry(record, metabib_field, index_vector)
266         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
267         FROM metabib.title_field_entry WHERE source = bib_id;
268
269     DELETE FROM metabib.combined_author_field_entry WHERE record = bib_id;
270     INSERT INTO metabib.combined_author_field_entry(record, metabib_field, index_vector)
271         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
272         FROM metabib.author_field_entry WHERE source = bib_id GROUP BY field;
273     INSERT INTO metabib.combined_author_field_entry(record, metabib_field, index_vector)
274         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
275         FROM metabib.author_field_entry WHERE source = bib_id;
276
277     DELETE FROM metabib.combined_subject_field_entry WHERE record = bib_id;
278     INSERT INTO metabib.combined_subject_field_entry(record, metabib_field, index_vector)
279         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
280         FROM metabib.subject_field_entry WHERE source = bib_id GROUP BY field;
281     INSERT INTO metabib.combined_subject_field_entry(record, metabib_field, index_vector)
282         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
283         FROM metabib.subject_field_entry WHERE source = bib_id;
284
285     DELETE FROM metabib.combined_series_field_entry WHERE record = bib_id;
286     INSERT INTO metabib.combined_series_field_entry(record, metabib_field, index_vector)
287         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
288         FROM metabib.series_field_entry WHERE source = bib_id GROUP BY field;
289     INSERT INTO metabib.combined_series_field_entry(record, metabib_field, index_vector)
290         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
291         FROM metabib.series_field_entry WHERE source = bib_id;
292
293     DELETE FROM metabib.combined_identifier_field_entry WHERE record = bib_id;
294     INSERT INTO metabib.combined_identifier_field_entry(record, metabib_field, index_vector)
295         SELECT bib_id, field, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
296         FROM metabib.identifier_field_entry WHERE source = bib_id GROUP BY field;
297     INSERT INTO metabib.combined_identifier_field_entry(record, metabib_field, index_vector)
298         SELECT bib_id, NULL, strip(COALESCE(string_agg(index_vector::TEXT,' '),'')::tsvector)
299         FROM metabib.identifier_field_entry WHERE source = bib_id;
300
301     -- For each virtual def, gather the data from the combined real field
302     -- entries and append it to the virtual combined entry.
303     FOR vfield, rfields IN SELECT virtual, ARRAY_AGG(real)  FROM config.metabib_field_virtual_map GROUP BY virtual LOOP
304         SELECT  field_class INTO vclass
305           FROM  config.metabib_field
306           WHERE id = vfield;
307
308         SELECT  string_agg(index_vector::TEXT,' ')::tsvector INTO rdata
309           FROM  metabib.combined_all_field_entry
310           WHERE record = bib_id
311                 AND metabib_field = ANY (rfields);
312
313         BEGIN -- I cannot wait for INSERT ON CONFLICT ... 9.5, though
314             EXECUTE $$
315                 INSERT INTO metabib.combined_$$ || vclass || $$_field_entry
316                     (record, metabib_field, index_vector) VALUES ($1, $2, $3)
317             $$ USING bib_id, vfield, rdata;
318         EXCEPTION WHEN unique_violation THEN
319             EXECUTE $$
320                 UPDATE  metabib.combined_$$ || vclass || $$_field_entry
321                   SET   index_vector = index_vector || $3
322                   WHERE record = $1
323                         AND metabib_field = $2
324             $$ USING bib_id, vfield, rdata;
325         END;
326     END LOOP;
327 END;
328 $func$ LANGUAGE PLPGSQL;
329
330 CREATE OR REPLACE VIEW search.best_tsconfig AS
331     SELECT  m.id AS id,
332             COALESCE(f.ts_config, c.ts_config, 'simple') AS ts_config
333       FROM  config.metabib_field m
334             LEFT JOIN config.metabib_class_ts_map c ON (c.field_class = m.field_class AND c.index_weight = 'C')
335             LEFT JOIN config.metabib_field_ts_map f ON (f.metabib_field = m.id AND c.index_weight = 'C');
336
337 CREATE TYPE search.highlight_result AS ( id BIGINT, source BIGINT, field INT, value TEXT, highlight TEXT );
338
339 CREATE OR REPLACE FUNCTION search.highlight_display_fields(
340     rid         BIGINT,
341     tsq         TEXT,
342     field_list  INT[] DEFAULT '{}'::INT[],
343     css_class   TEXT DEFAULT 'oils_SH',
344     hl_all      BOOL DEFAULT TRUE,
345     minwords    INT DEFAULT 5,
346     maxwords    INT DEFAULT 25,
347     shortwords  INT DEFAULT 0,
348     maxfrags    INT DEFAULT 0,
349     delimiter   TEXT DEFAULT ' ... '
350 ) RETURNS SETOF search.highlight_result AS $f$
351 DECLARE
352     opts            TEXT := '';
353     v_css_class     TEXT := css_class;
354     v_delimiter     TEXT := delimiter;
355     v_field_list    INT[] := field_list;
356     hl_query        TEXT;
357 BEGIN
358     IF v_delimiter LIKE $$%'%$$ OR v_delimiter LIKE '%"%' THEN --"
359         v_delimiter := ' ... ';
360     END IF;
361
362     IF NOT hl_all THEN
363         opts := opts || 'MinWords=' || minwords;
364         opts := opts || ', MaxWords=' || maxwords;
365         opts := opts || ', ShortWords=' || shortwords;
366         opts := opts || ', MaxFragments=' || maxfrags;
367         opts := opts || ', FragmentDelimiter="' || delimiter || '"';
368     ELSE
369         opts := opts || 'HighlightAll=TRUE';
370     END IF;
371
372     IF v_css_class LIKE $$%'%$$ OR v_css_class LIKE '%"%' THEN -- "
373         v_css_class := 'oils_SH';
374     END IF;
375
376     opts := opts || $$, StopSel=</b>, StartSel="<b class='$$ || v_css_class; -- "
377
378     IF v_field_list = '{}'::INT[] THEN
379         SELECT ARRAY_AGG(id) INTO v_field_list FROM config.metabib_field WHERE display_field;
380     END IF;
381
382     hl_query := $$
383         SELECT  de.id,
384                 de.source,
385                 de.field,
386                 de.value AS value,
387                 ts_headline(
388                     ts_config::REGCONFIG,
389                     evergreen.escape_for_html(de.value),
390                     $$ || quote_literal(tsq) || $$,
391                     $1 || ' ' || mf.field_class || ' ' || mf.name || $xx$'>"$xx$ -- "'
392                 ) AS highlight
393           FROM  metabib.display_entry de
394                 JOIN config.metabib_field mf ON (mf.id = de.field)
395                 JOIN search.best_tsconfig t ON (t.id = de.field)
396           WHERE de.source = $2
397                 AND field = ANY ($3)
398           ORDER BY de.id;$$;
399
400     RETURN QUERY EXECUTE hl_query USING opts, rid, v_field_list;
401 END;
402 $f$ LANGUAGE PLPGSQL;
403
404 CREATE OR REPLACE FUNCTION evergreen.escape_for_html (TEXT) RETURNS TEXT AS $$
405     SELECT  regexp_replace(
406                 regexp_replace(
407                     regexp_replace(
408                         $1,
409                         '&',
410                         '&amp;',
411                         'g'
412                     ),
413                     '<',
414                     '&lt;',
415                     'g'
416                 ),
417                 '>',
418                 '&gt;',
419                 'g'
420             );
421 $$ LANGUAGE SQL IMMUTABLE LEAKPROOF STRICT COST 10;
422
423 CREATE OR REPLACE FUNCTION search.highlight_display_fields(
424     rid         BIGINT,
425     tsq_map     HSTORE, -- { '(a | b) & c' => '1,2,3,4', ...}
426     css_class   TEXT DEFAULT 'oils_SH',
427     hl_all      BOOL DEFAULT TRUE,
428     minwords    INT DEFAULT 5,
429     maxwords    INT DEFAULT 25,
430     shortwords  INT DEFAULT 0,
431     maxfrags    INT DEFAULT 0,
432     delimiter   TEXT DEFAULT ' ... '
433 ) RETURNS SETOF search.highlight_result AS $f$
434 DECLARE
435     tsq     TEXT;
436     fields  TEXT;
437     afields INT[];
438     seen    INT[];
439 BEGIN
440     FOR tsq, fields IN SELECT key, value FROM each(tsq_map) LOOP
441         SELECT  ARRAY_AGG(unnest::INT) INTO afields
442           FROM  unnest(regexp_split_to_array(fields,','));
443         seen := seen || afields;
444
445         RETURN QUERY
446             SELECT * FROM search.highlight_display_fields(
447                 rid, tsq, afields, css_class, hl_all,minwords,
448                 maxwords, shortwords, maxfrags, delimiter
449             );
450     END LOOP;
451
452     RETURN QUERY
453         SELECT  id,
454                 source,
455                 field,
456                 value,
457                 value AS highlight
458           FROM  metabib.display_entry
459           WHERE source = rid
460                 AND NOT (field = ANY (seen));
461 END;
462 $f$ LANGUAGE PLPGSQL ROWS 10;
463  
464 CREATE OR REPLACE FUNCTION metabib.remap_metarecord_for_bib(
465     bib_id bigint,
466     fp text,
467     bib_is_deleted boolean DEFAULT false,
468     retain_deleted boolean DEFAULT false
469 ) RETURNS bigint AS $function$
470 DECLARE
471     new_mapping     BOOL := TRUE;
472     source_count    INT;
473     old_mr          BIGINT;
474     tmp_mr          metabib.metarecord%ROWTYPE;
475     deleted_mrs     BIGINT[];
476 BEGIN
477
478     -- We need to make sure we're not a deleted master record of an MR
479     IF bib_is_deleted THEN
480         IF NOT retain_deleted THEN -- Go away for any MR that we're master of, unless retained
481             DELETE FROM metabib.metarecord_source_map WHERE source = bib_id;
482         END IF;
483
484         FOR old_mr IN SELECT id FROM metabib.metarecord WHERE master_record = bib_id LOOP
485
486             -- Now, are there any more sources on this MR?
487             SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = old_mr;
488
489             IF source_count = 0 AND NOT retain_deleted THEN -- No other records
490                 deleted_mrs := ARRAY_APPEND(deleted_mrs, old_mr); -- Just in case...
491                 DELETE FROM metabib.metarecord WHERE id = old_mr;
492
493             ELSE -- indeed there are. Update it with a null cache and recalcualated master record
494                 UPDATE  metabib.metarecord
495                   SET   mods = NULL,
496                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
497                   WHERE id = old_mr;
498             END IF;
499         END LOOP;
500
501     ELSE -- insert or update
502
503         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
504
505             -- Find the first fingerprint-matching
506             IF old_mr IS NULL AND fp = tmp_mr.fingerprint THEN
507                 old_mr := tmp_mr.id;
508                 new_mapping := FALSE;
509
510             ELSE -- Our fingerprint changed ... maybe remove the old MR
511                 DELETE FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id AND source = bib_id; -- remove the old source mapping
512                 SELECT COUNT(*) INTO source_count FROM metabib.metarecord_source_map WHERE metarecord = tmp_mr.id;
513                 IF source_count = 0 THEN -- No other records
514                     deleted_mrs := ARRAY_APPEND(deleted_mrs, tmp_mr.id);
515                     DELETE FROM metabib.metarecord WHERE id = tmp_mr.id;
516                 END IF;
517             END IF;
518
519         END LOOP;
520
521         -- we found no suitable, preexisting MR based on old source maps
522         IF old_mr IS NULL THEN
523             SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp; -- is there one for our current fingerprint?
524
525             IF old_mr IS NULL THEN -- nope, create one and grab its id
526                 INSERT INTO metabib.metarecord ( fingerprint, master_record ) VALUES ( fp, bib_id );
527                 SELECT id INTO old_mr FROM metabib.metarecord WHERE fingerprint = fp;
528
529             ELSE -- indeed there is. update it with a null cache and recalcualated master record
530                 UPDATE  metabib.metarecord
531                   SET   mods = NULL,
532                         master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
533                   WHERE id = old_mr;
534             END IF;
535
536         ELSE -- there was one we already attached to, update its mods cache and master_record
537             UPDATE  metabib.metarecord
538               SET   mods = NULL,
539                     master_record = ( SELECT id FROM biblio.record_entry WHERE fingerprint = fp AND NOT deleted ORDER BY quality DESC LIMIT 1)
540               WHERE id = old_mr;
541         END IF;
542
543         IF new_mapping THEN
544             INSERT INTO metabib.metarecord_source_map (metarecord, source) VALUES (old_mr, bib_id); -- new source mapping
545         END IF;
546
547     END IF;
548
549     IF ARRAY_UPPER(deleted_mrs,1) > 0 THEN
550         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
551     END IF;
552
553     RETURN old_mr;
554
555 END;
556 $function$ LANGUAGE plpgsql;
557
558 COMMIT;
559