]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/upgrade/0855.change_to_array_agg.sql
LP#1117808: release notes for New Access points for MARC Overlay
[Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / 0855.change_to_array_agg.sql
1 -- Compiled list of all changed functions and views where we went from:
2 --   array_accum() to array_agg()
3 --   array_to_string(array_agg()) to string_agg()
4
5 BEGIN;
6
7 SELECT evergreen.upgrade_deps_block_check('0855', :eg_version);
8
9 -- from 000.functions.general.sql
10
11 CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT) RETURNS anyarray AS $$ SELECT ARRAY_AGG(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2; $$ LANGUAGE SQL;
12
13
14 -- from 002.functions.config.sql
15
16 CREATE OR REPLACE FUNCTION public.extract_marc_field ( TEXT, BIGINT, TEXT, TEXT ) RETURNS TEXT AS $$
17     SELECT regexp_replace(string_agg(output,' '),$4,'','g') FROM oils_xpath_table('id', 'marc', $1, $3, 'id='||$2)x(id INT, output TEXT);
18 $$ LANGUAGE SQL;
19
20
21 -- from 011.schema.authority.sql
22
23 CREATE OR REPLACE FUNCTION authority.axis_authority_tags(a TEXT) RETURNS INT[] AS $$
24     SELECT ARRAY_AGG(field) FROM authority.browse_axis_authority_field_map WHERE axis = $1;
25 $$ LANGUAGE SQL;
26
27 CREATE OR REPLACE FUNCTION authority.axis_authority_tags_refs(a TEXT) RETURNS INT[] AS $$
28     SELECT ARRAY_AGG(y) from (
29        SELECT  unnest(ARRAY_CAT(
30                  ARRAY[a.field],
31                  (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.field)
32              )) y
33        FROM  authority.browse_axis_authority_field_map a
34        WHERE axis = $1) x
35 $$ LANGUAGE SQL;
36
37 CREATE OR REPLACE FUNCTION authority.btag_authority_tags(btag TEXT) RETURNS INT[] AS $$
38     SELECT ARRAY_AGG(authority_field) FROM authority.control_set_bib_field WHERE tag = $1
39 $$ LANGUAGE SQL;
40
41 CREATE OR REPLACE FUNCTION authority.btag_authority_tags_refs(btag TEXT) RETURNS INT[] AS $$
42     SELECT ARRAY_AGG(y) from (
43         SELECT  unnest(ARRAY_CAT(
44                     ARRAY[a.authority_field],
45                     (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.authority_field)
46                 )) y
47       FROM  authority.control_set_bib_field a
48       WHERE a.tag = $1) x
49 $$ LANGUAGE SQL;
50
51 CREATE OR REPLACE FUNCTION authority.atag_authority_tags(atag TEXT) RETURNS INT[] AS $$
52     SELECT ARRAY_AGG(id) FROM authority.control_set_authority_field WHERE tag = $1
53 $$ LANGUAGE SQL;
54
55 CREATE OR REPLACE FUNCTION authority.atag_authority_tags_refs(atag TEXT) RETURNS INT[] AS $$
56     SELECT ARRAY_AGG(y) from (
57         SELECT  unnest(ARRAY_CAT(
58                     ARRAY[a.id],
59                     (SELECT ARRAY_AGG(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.id)
60                 )) y
61       FROM  authority.control_set_authority_field a
62       WHERE a.tag = $1) x
63 $$ LANGUAGE SQL;
64
65
66 -- from 012.schema.vandelay.sql
67
68 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT, attr_defs TEXT[]) RETURNS hstore AS $_$
69 DECLARE
70     transformed_xml TEXT;
71     prev_xfrm       TEXT;
72     normalizer      RECORD;
73     xfrm            config.xml_transform%ROWTYPE;
74     attr_value      TEXT;
75     new_attrs       HSTORE := ''::HSTORE;
76     attr_def        config.record_attr_definition%ROWTYPE;
77 BEGIN
78
79     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE name IN (SELECT * FROM UNNEST(attr_defs)) ORDER BY format LOOP
80
81         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
82             SELECT  STRING_AGG(x.value, COALESCE(attr_def.joiner,' ')) INTO attr_value
83               FROM  vandelay.flatten_marc(xml) AS x
84               WHERE x.tag LIKE attr_def.tag
85                     AND CASE
86                         WHEN attr_def.sf_list IS NOT NULL
87                             THEN POSITION(x.subfield IN attr_def.sf_list) > 0
88                         ELSE TRUE
89                         END
90               GROUP BY x.tag
91               ORDER BY x.tag
92               LIMIT 1;
93
94         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
95             attr_value := vandelay.marc21_extract_fixed_field(xml, attr_def.fixed_field);
96
97         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
98
99             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
100
101             -- See if we can skip the XSLT ... it's expensive
102             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
103                 -- Can't skip the transform
104                 IF xfrm.xslt <> '---' THEN
105                     transformed_xml := oils_xslt_process(xml,xfrm.xslt);
106                 ELSE
107                     transformed_xml := xml;
108                 END IF;
109
110                 prev_xfrm := xfrm.name;
111             END IF;
112
113             IF xfrm.name IS NULL THEN
114                 -- just grab the marcxml (empty) transform
115                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
116                 prev_xfrm := xfrm.name;
117             END IF;
118
119             attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
120
121         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
122             SELECT  m.value::TEXT INTO attr_value
123               FROM  vandelay.marc21_physical_characteristics(xml) v
124                     JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
125               WHERE v.subfield = attr_def.phys_char_sf
126               LIMIT 1; -- Just in case ...
127
128         END IF;
129
130         -- apply index normalizers to attr_value
131         FOR normalizer IN
132             SELECT  n.func AS func,
133                     n.param_count AS param_count,
134                     m.params AS params
135               FROM  config.index_normalizer n
136                     JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
137               WHERE attr = attr_def.name
138               ORDER BY m.pos LOOP
139                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
140                     quote_nullable( attr_value ) ||
141                     CASE
142                         WHEN normalizer.param_count > 0
143                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
144                             ELSE ''
145                         END ||
146                     ')' INTO attr_value;
147
148         END LOOP;
149
150         -- Add the new value to the hstore
151         new_attrs := new_attrs || hstore( attr_def.name, attr_value );
152
153     END LOOP;
154
155     RETURN new_attrs;
156 END;
157 $_$ LANGUAGE PLPGSQL;
158
159 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT ) RETURNS hstore AS $_$
160     SELECT vandelay.extract_rec_attrs( $1, (SELECT ARRAY_AGG(name) FROM config.record_attr_definition));
161 $_$ LANGUAGE SQL;
162
163 CREATE OR REPLACE FUNCTION vandelay.match_set_test_marcxml(
164     match_set_id INTEGER, record_xml TEXT, bucket_id INTEGER 
165 ) RETURNS SETOF vandelay.match_set_test_result AS $$
166 DECLARE
167     tags_rstore HSTORE;
168     svf_rstore  HSTORE;
169     coal        TEXT;
170     joins       TEXT;
171     query_      TEXT;
172     wq          TEXT;
173     qvalue      INTEGER;
174     rec         RECORD;
175 BEGIN
176     tags_rstore := vandelay.flatten_marc_hstore(record_xml);
177     svf_rstore := vandelay.extract_rec_attrs(record_xml);
178
179     CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
180     CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
181
182     -- generate the where clause and return that directly (into wq), and as
183     -- a side-effect, populate the _vandelay_tmp_[qj]rows tables.
184     wq := vandelay.get_expr_from_match_set(match_set_id, tags_rstore);
185
186     query_ := 'SELECT DISTINCT(record), ';
187
188     -- qrows table is for the quality bits we add to the SELECT clause
189     SELECT STRING_AGG(
190         'COALESCE(n' || q::TEXT || '.quality, 0)', ' + '
191     ) INTO coal FROM _vandelay_tmp_qrows;
192
193     -- our query string so far is the SELECT clause and the inital FROM.
194     -- no JOINs yet nor the WHERE clause
195     query_ := query_ || coal || ' AS quality ' || E'\n';
196
197     -- jrows table is for the joins we must make (and the real text conditions)
198     SELECT STRING_AGG(j, E'\n') INTO joins
199         FROM _vandelay_tmp_jrows;
200
201     -- add those joins and the where clause to our query.
202     query_ := query_ || joins || E'\n';
203
204     -- join the record bucket
205     IF bucket_id IS NOT NULL THEN
206         query_ := query_ || 'JOIN container.biblio_record_entry_bucket_item ' ||
207             'brebi ON (brebi.target_biblio_record_entry = record ' ||
208             'AND brebi.bucket = ' || bucket_id || E')\n';
209     END IF;
210
211     query_ := query_ || 'JOIN biblio.record_entry bre ON (bre.id = record) ' || 'WHERE ' || wq || ' AND not bre.deleted';
212
213     -- this will return rows of record,quality
214     FOR rec IN EXECUTE query_ USING tags_rstore, svf_rstore LOOP
215         RETURN NEXT rec;
216     END LOOP;
217
218     DROP TABLE _vandelay_tmp_qrows;
219     DROP TABLE _vandelay_tmp_jrows;
220     RETURN;
221 END;
222 $$ LANGUAGE PLPGSQL;
223
224 CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
225     record_xml TEXT
226 ) RETURNS HSTORE AS $func$
227 BEGIN
228     RETURN (SELECT
229         HSTORE(
230             ARRAY_AGG(tag || (COALESCE(subfield, ''))),
231             ARRAY_AGG(value)
232         )
233         FROM (
234             SELECT  tag, subfield, ARRAY_AGG(value)::TEXT AS value
235               FROM  (SELECT tag,
236                             subfield,
237                             CASE WHEN tag = '020' THEN -- caseless -- isbn
238                                 LOWER((REGEXP_MATCHES(value,$$^(\S{10,17})$$))[1] || '%')
239                             WHEN tag = '022' THEN -- caseless -- issn
240                                 LOWER((REGEXP_MATCHES(value,$$^(\S{4}[- ]?\S{4})$$))[1] || '%')
241                             WHEN tag = '024' THEN -- caseless -- upc (other)
242                                 LOWER(value || '%')
243                             ELSE
244                                 value
245                             END AS value
246                       FROM  vandelay.flatten_marc(record_xml)) x
247                 GROUP BY tag, subfield ORDER BY tag, subfield
248         ) subquery
249     );
250 END;
251 $func$ LANGUAGE PLPGSQL;
252
253 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set_point(
254     node vandelay.match_set_point,
255     tags_rstore HSTORE
256 ) RETURNS TEXT AS $$
257 DECLARE
258     q           TEXT;
259     i           INTEGER;
260     this_op     TEXT;
261     children    INTEGER[];
262     child       vandelay.match_set_point;
263 BEGIN
264     SELECT ARRAY_AGG(id) INTO children FROM vandelay.match_set_point
265         WHERE parent = node.id;
266
267     IF ARRAY_LENGTH(children, 1) > 0 THEN
268         this_op := vandelay._get_expr_render_one(node);
269         q := '(';
270         i := 1;
271         WHILE children[i] IS NOT NULL LOOP
272             SELECT * INTO child FROM vandelay.match_set_point
273                 WHERE id = children[i];
274             IF i > 1 THEN
275                 q := q || ' ' || this_op || ' ';
276             END IF;
277             i := i + 1;
278             q := q || vandelay.get_expr_from_match_set_point(child, tags_rstore);
279         END LOOP;
280         q := q || ')';
281         RETURN q;
282     ELSIF node.bool_op IS NULL THEN
283         PERFORM vandelay._get_expr_push_qrow(node);
284         PERFORM vandelay._get_expr_push_jrow(node, tags_rstore);
285         RETURN vandelay._get_expr_render_one(node);
286     ELSE
287         RETURN '';
288     END IF;
289 END;
290 $$  LANGUAGE PLPGSQL;
291
292
293 -- from 030.schema.metabib.sql
294
295 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
296 DECLARE
297     uris            TEXT[];
298     uri_xml         TEXT;
299     uri_label       TEXT;
300     uri_href        TEXT;
301     uri_use         TEXT;
302     uri_owner_list  TEXT[];
303     uri_owner       TEXT;
304     uri_owner_id    INT;
305     uri_id          INT;
306     uri_cn_id       INT;
307     uri_map_id      INT;
308 BEGIN
309
310     -- Clear any URI mappings and call numbers for this bib.
311     -- This leads to acn / auricnm inflation, but also enables
312     -- old acn/auricnm's to go away and for bibs to be deleted.
313     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
314         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
315         DELETE FROM asset.call_number WHERE id = uri_cn_id;
316     END LOOP;
317
318     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
319     IF ARRAY_UPPER(uris,1) > 0 THEN
320         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
321             -- First we pull info out of the 856
322             uri_xml     := uris[i];
323
324             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
325             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()',uri_xml))[1];
326             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
327
328             IF uri_label IS NULL THEN
329                 uri_label := uri_href;
330             END IF;
331             CONTINUE WHEN uri_href IS NULL;
332
333             -- Get the distinct list of libraries wanting to use 
334             SELECT  ARRAY_AGG(
335                         DISTINCT REGEXP_REPLACE(
336                             x,
337                             $re$^.*?\((\w+)\).*$$re$,
338                             E'\\1'
339                         )
340                     ) INTO uri_owner_list
341               FROM  UNNEST(
342                         oils_xpath(
343                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
344                             uri_xml
345                         )
346                     )x;
347
348             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
349
350                 -- look for a matching uri
351                 IF uri_use IS NULL THEN
352                     SELECT id INTO uri_id
353                         FROM asset.uri
354                         WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active
355                         ORDER BY id LIMIT 1;
356                     IF NOT FOUND THEN -- create one
357                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
358                         SELECT id INTO uri_id
359                             FROM asset.uri
360                             WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
361                     END IF;
362                 ELSE
363                     SELECT id INTO uri_id
364                         FROM asset.uri
365                         WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active
366                         ORDER BY id LIMIT 1;
367                     IF NOT FOUND THEN -- create one
368                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
369                         SELECT id INTO uri_id
370                             FROM asset.uri
371                             WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
372                     END IF;
373                 END IF;
374
375                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
376                     uri_owner := uri_owner_list[j];
377
378                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
379                     CONTINUE WHEN NOT FOUND;
380
381                     -- we need a call number to link through
382                     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;
383                     IF NOT FOUND THEN
384                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
385                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
386                         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;
387                     END IF;
388
389                     -- now, link them if they're not already
390                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
391                     IF NOT FOUND THEN
392                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
393                     END IF;
394
395                 END LOOP;
396
397             END IF;
398
399         END LOOP;
400     END IF;
401
402     RETURN;
403 END;
404 $func$ LANGUAGE PLPGSQL;
405
406 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
407 DECLARE
408     transformed_xml TEXT;
409     prev_xfrm       TEXT;
410     normalizer      RECORD;
411     xfrm            config.xml_transform%ROWTYPE;
412     attr_value      TEXT;
413     new_attrs       HSTORE := ''::HSTORE;
414     attr_def        config.record_attr_definition%ROWTYPE;
415 BEGIN
416
417     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
418         PERFORM * FROM config.internal_flag WHERE
419             name = 'ingest.metarecord_mapping.preserve_on_delete' AND enabled;
420         IF NOT FOUND THEN
421             -- One needs to keep these around to support searches
422             -- with the #deleted modifier, so one should turn on the named
423             -- internal flag for that functionality.
424             DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id;
425             DELETE FROM metabib.record_attr WHERE id = NEW.id;
426         END IF;
427
428         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
429         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
430         DELETE FROM metabib.browse_entry_def_map WHERE source = NEW.id; -- Don't auto-suggest deleted bibs
431         RETURN NEW; -- and we're done
432     END IF;
433
434     IF TG_OP = 'UPDATE' THEN -- re-ingest?
435         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
436
437         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
438             RETURN NEW;
439         END IF;
440     END IF;
441
442     -- Record authority linking
443     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
444     IF NOT FOUND THEN
445         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
446     END IF;
447
448     -- Flatten and insert the mfr data
449     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
450     IF NOT FOUND THEN
451         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
452
453         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
454         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
455         IF NOT FOUND THEN
456             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
457
458                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
459                     SELECT  STRING_AGG(value, COALESCE(attr_def.joiner,' ')) INTO attr_value
460                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
461                       WHERE record = NEW.id
462                             AND tag LIKE attr_def.tag
463                             AND CASE
464                                 WHEN attr_def.sf_list IS NOT NULL 
465                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
466                                 ELSE TRUE
467                                 END
468                       GROUP BY tag
469                       ORDER BY tag
470                       LIMIT 1;
471
472                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
473                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
474
475                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
476
477                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
478             
479                     -- See if we can skip the XSLT ... it's expensive
480                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
481                         -- Can't skip the transform
482                         IF xfrm.xslt <> '---' THEN
483                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
484                         ELSE
485                             transformed_xml := NEW.marc;
486                         END IF;
487             
488                         prev_xfrm := xfrm.name;
489                     END IF;
490
491                     IF xfrm.name IS NULL THEN
492                         -- just grab the marcxml (empty) transform
493                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
494                         prev_xfrm := xfrm.name;
495                     END IF;
496
497                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
498
499                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
500                     SELECT  m.value INTO attr_value
501                       FROM  biblio.marc21_physical_characteristics(NEW.id) v
502                             JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
503                       WHERE v.subfield = attr_def.phys_char_sf
504                       LIMIT 1; -- Just in case ...
505
506                 END IF;
507
508                 -- apply index normalizers to attr_value
509                 FOR normalizer IN
510                     SELECT  n.func AS func,
511                             n.param_count AS param_count,
512                             m.params AS params
513                       FROM  config.index_normalizer n
514                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
515                       WHERE attr = attr_def.name
516                       ORDER BY m.pos LOOP
517                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
518                             COALESCE( quote_literal( attr_value ), 'NULL' ) ||
519                             CASE
520                                 WHEN normalizer.param_count > 0
521                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
522                                     ELSE ''
523                                 END ||
524                             ')' INTO attr_value;
525         
526                 END LOOP;
527
528                 -- Add the new value to the hstore
529                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
530
531             END LOOP;
532
533             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
534                 DELETE FROM metabib.record_attr WHERE id = NEW.id;
535                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
536             ELSE
537                 UPDATE metabib.record_attr SET attrs = new_attrs WHERE id = NEW.id;
538             END IF;
539
540         END IF;
541     END IF;
542
543     -- Gather and insert the field entry data
544     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
545
546     -- Located URI magic
547     IF TG_OP = 'INSERT' THEN
548         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
549         IF NOT FOUND THEN
550             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
551         END IF;
552     ELSE
553         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
554         IF NOT FOUND THEN
555             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
556         END IF;
557     END IF;
558
559     -- (re)map metarecord-bib linking
560     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
561         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
562         IF NOT FOUND THEN
563             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
564         END IF;
565     ELSE -- we're doing an update, and we're not deleted, remap
566         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
567         IF NOT FOUND THEN
568             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
569         END IF;
570     END IF;
571
572     RETURN NEW;
573 END;
574 $func$ LANGUAGE PLPGSQL;
575
576
577 -- from 100.circ_matrix.sql
578
579 CREATE OR REPLACE FUNCTION actor.calculate_system_penalties( match_user INT, context_org INT ) RETURNS SETOF actor.usr_standing_penalty AS $func$
580 DECLARE
581     user_object         actor.usr%ROWTYPE;
582     new_sp_row          actor.usr_standing_penalty%ROWTYPE;
583     existing_sp_row     actor.usr_standing_penalty%ROWTYPE;
584     collections_fines   permission.grp_penalty_threshold%ROWTYPE;
585     max_fines           permission.grp_penalty_threshold%ROWTYPE;
586     max_overdue         permission.grp_penalty_threshold%ROWTYPE;
587     max_items_out       permission.grp_penalty_threshold%ROWTYPE;
588     max_lost            permission.grp_penalty_threshold%ROWTYPE;
589     max_longoverdue     permission.grp_penalty_threshold%ROWTYPE;
590     tmp_grp             INT;
591     items_overdue       INT;
592     items_out           INT;
593     items_lost          INT;
594     items_longoverdue   INT;
595     context_org_list    INT[];
596     current_fines        NUMERIC(8,2) := 0.0;
597     tmp_fines            NUMERIC(8,2);
598     tmp_groc            RECORD;
599     tmp_circ            RECORD;
600     tmp_org             actor.org_unit%ROWTYPE;
601     tmp_penalty         config.standing_penalty%ROWTYPE;
602     tmp_depth           INTEGER;
603 BEGIN
604     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
605
606     -- Max fines
607     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
608
609     -- Fail if the user has a high fine balance
610     LOOP
611         tmp_grp := user_object.profile;
612         LOOP
613             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 1 AND org_unit = tmp_org.id;
614
615             IF max_fines.threshold IS NULL THEN
616                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
617             ELSE
618                 EXIT;
619             END IF;
620
621             IF tmp_grp IS NULL THEN
622                 EXIT;
623             END IF;
624         END LOOP;
625
626         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
627             EXIT;
628         END IF;
629
630         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
631
632     END LOOP;
633
634     IF max_fines.threshold IS NOT NULL THEN
635
636         RETURN QUERY
637             SELECT  *
638               FROM  actor.usr_standing_penalty
639               WHERE usr = match_user
640                     AND org_unit = max_fines.org_unit
641                     AND (stop_date IS NULL or stop_date > NOW())
642                     AND standing_penalty = 1;
643
644         SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( max_fines.org_unit );
645
646         SELECT  SUM(f.balance_owed) INTO current_fines
647           FROM  money.materialized_billable_xact_summary f
648                 JOIN (
649                     SELECT  r.id
650                       FROM  booking.reservation r
651                       WHERE r.usr = match_user
652                             AND r.pickup_lib IN (SELECT * FROM unnest(context_org_list))
653                             AND xact_finish IS NULL
654                                 UNION ALL
655                     SELECT  g.id
656                       FROM  money.grocery g
657                       WHERE g.usr = match_user
658                             AND g.billing_location IN (SELECT * FROM unnest(context_org_list))
659                             AND xact_finish IS NULL
660                                 UNION ALL
661                     SELECT  circ.id
662                       FROM  action.circulation circ
663                       WHERE circ.usr = match_user
664                             AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
665                             AND xact_finish IS NULL ) l USING (id);
666
667         IF current_fines >= max_fines.threshold THEN
668             new_sp_row.usr := match_user;
669             new_sp_row.org_unit := max_fines.org_unit;
670             new_sp_row.standing_penalty := 1;
671             RETURN NEXT new_sp_row;
672         END IF;
673     END IF;
674
675     -- Start over for max overdue
676     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
677
678     -- Fail if the user has too many overdue items
679     LOOP
680         tmp_grp := user_object.profile;
681         LOOP
682
683             SELECT * INTO max_overdue FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 2 AND org_unit = tmp_org.id;
684
685             IF max_overdue.threshold IS NULL THEN
686                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
687             ELSE
688                 EXIT;
689             END IF;
690
691             IF tmp_grp IS NULL THEN
692                 EXIT;
693             END IF;
694         END LOOP;
695
696         IF max_overdue.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
697             EXIT;
698         END IF;
699
700         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
701
702     END LOOP;
703
704     IF max_overdue.threshold IS NOT NULL THEN
705
706         RETURN QUERY
707             SELECT  *
708               FROM  actor.usr_standing_penalty
709               WHERE usr = match_user
710                     AND org_unit = max_overdue.org_unit
711                     AND (stop_date IS NULL or stop_date > NOW())
712                     AND standing_penalty = 2;
713
714         SELECT  INTO items_overdue COUNT(*)
715           FROM  action.circulation circ
716                 JOIN  actor.org_unit_full_path( max_overdue.org_unit ) fp ON (circ.circ_lib = fp.id)
717           WHERE circ.usr = match_user
718             AND circ.checkin_time IS NULL
719             AND circ.due_date < NOW()
720             AND (circ.stop_fines = 'MAXFINES' OR circ.stop_fines IS NULL);
721
722         IF items_overdue >= max_overdue.threshold::INT THEN
723             new_sp_row.usr := match_user;
724             new_sp_row.org_unit := max_overdue.org_unit;
725             new_sp_row.standing_penalty := 2;
726             RETURN NEXT new_sp_row;
727         END IF;
728     END IF;
729
730     -- Start over for max out
731     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
732
733     -- Fail if the user has too many checked out items
734     LOOP
735         tmp_grp := user_object.profile;
736         LOOP
737             SELECT * INTO max_items_out FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 3 AND org_unit = tmp_org.id;
738
739             IF max_items_out.threshold IS NULL THEN
740                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
741             ELSE
742                 EXIT;
743             END IF;
744
745             IF tmp_grp IS NULL THEN
746                 EXIT;
747             END IF;
748         END LOOP;
749
750         IF max_items_out.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
751             EXIT;
752         END IF;
753
754         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
755
756     END LOOP;
757
758
759     -- Fail if the user has too many items checked out
760     IF max_items_out.threshold IS NOT NULL THEN
761
762         RETURN QUERY
763             SELECT  *
764               FROM  actor.usr_standing_penalty
765               WHERE usr = match_user
766                     AND org_unit = max_items_out.org_unit
767                     AND (stop_date IS NULL or stop_date > NOW())
768                     AND standing_penalty = 3;
769
770         SELECT  INTO items_out COUNT(*)
771           FROM  action.circulation circ
772                 JOIN  actor.org_unit_full_path( max_items_out.org_unit ) fp ON (circ.circ_lib = fp.id)
773           WHERE circ.usr = match_user
774                 AND circ.checkin_time IS NULL
775                 AND (circ.stop_fines IN (
776                     SELECT 'MAXFINES'::TEXT
777                     UNION ALL
778                     SELECT 'LONGOVERDUE'::TEXT
779                     UNION ALL
780                     SELECT 'LOST'::TEXT
781                     WHERE 'true' ILIKE
782                     (
783                         SELECT CASE
784                             WHEN (SELECT value FROM actor.org_unit_ancestor_setting('circ.tally_lost', circ.circ_lib)) ILIKE 'true' THEN 'true'
785                             ELSE 'false'
786                         END
787                     )
788                     UNION ALL
789                     SELECT 'CLAIMSRETURNED'::TEXT
790                     WHERE 'false' ILIKE
791                     (
792                         SELECT CASE
793                             WHEN (SELECT value FROM actor.org_unit_ancestor_setting('circ.do_not_tally_claims_returned', circ.circ_lib)) ILIKE 'true' THEN 'true'
794                             ELSE 'false'
795                         END
796                     )
797                     ) OR circ.stop_fines IS NULL)
798                 AND xact_finish IS NULL;
799
800            IF items_out >= max_items_out.threshold::INT THEN
801             new_sp_row.usr := match_user;
802             new_sp_row.org_unit := max_items_out.org_unit;
803             new_sp_row.standing_penalty := 3;
804             RETURN NEXT new_sp_row;
805            END IF;
806     END IF;
807
808     -- Start over for max lost
809     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
810
811     -- Fail if the user has too many lost items
812     LOOP
813         tmp_grp := user_object.profile;
814         LOOP
815
816             SELECT * INTO max_lost FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 5 AND org_unit = tmp_org.id;
817
818             IF max_lost.threshold IS NULL THEN
819                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
820             ELSE
821                 EXIT;
822             END IF;
823
824             IF tmp_grp IS NULL THEN
825                 EXIT;
826             END IF;
827         END LOOP;
828
829         IF max_lost.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
830             EXIT;
831         END IF;
832
833         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
834
835     END LOOP;
836
837     IF max_lost.threshold IS NOT NULL THEN
838
839         RETURN QUERY
840             SELECT  *
841             FROM  actor.usr_standing_penalty
842             WHERE usr = match_user
843                 AND org_unit = max_lost.org_unit
844                 AND (stop_date IS NULL or stop_date > NOW())
845                 AND standing_penalty = 5;
846
847         SELECT  INTO items_lost COUNT(*)
848         FROM  action.circulation circ
849             JOIN  actor.org_unit_full_path( max_lost.org_unit ) fp ON (circ.circ_lib = fp.id)
850         WHERE circ.usr = match_user
851             AND circ.checkin_time IS NULL
852             AND (circ.stop_fines = 'LOST')
853             AND xact_finish IS NULL;
854
855         IF items_lost >= max_lost.threshold::INT AND 0 < max_lost.threshold::INT THEN
856             new_sp_row.usr := match_user;
857             new_sp_row.org_unit := max_lost.org_unit;
858             new_sp_row.standing_penalty := 5;
859             RETURN NEXT new_sp_row;
860         END IF;
861     END IF;
862
863     -- Start over for max longoverdue
864     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
865
866     -- Fail if the user has too many longoverdue items
867     LOOP
868         tmp_grp := user_object.profile;
869         LOOP
870
871             SELECT * INTO max_longoverdue 
872                 FROM permission.grp_penalty_threshold 
873                 WHERE grp = tmp_grp AND 
874                     penalty = 35 AND 
875                     org_unit = tmp_org.id;
876
877             IF max_longoverdue.threshold IS NULL THEN
878                 SELECT parent INTO tmp_grp 
879                     FROM permission.grp_tree WHERE id = tmp_grp;
880             ELSE
881                 EXIT;
882             END IF;
883
884             IF tmp_grp IS NULL THEN
885                 EXIT;
886             END IF;
887         END LOOP;
888
889         IF max_longoverdue.threshold IS NOT NULL 
890                 OR tmp_org.parent_ou IS NULL THEN
891             EXIT;
892         END IF;
893
894         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
895
896     END LOOP;
897
898     IF max_longoverdue.threshold IS NOT NULL THEN
899
900         RETURN QUERY
901             SELECT  *
902             FROM  actor.usr_standing_penalty
903             WHERE usr = match_user
904                 AND org_unit = max_longoverdue.org_unit
905                 AND (stop_date IS NULL or stop_date > NOW())
906                 AND standing_penalty = 35;
907
908         SELECT INTO items_longoverdue COUNT(*)
909         FROM action.circulation circ
910             JOIN actor.org_unit_full_path( max_longoverdue.org_unit ) fp 
911                 ON (circ.circ_lib = fp.id)
912         WHERE circ.usr = match_user
913             AND circ.checkin_time IS NULL
914             AND (circ.stop_fines = 'LONGOVERDUE')
915             AND xact_finish IS NULL;
916
917         IF items_longoverdue >= max_longoverdue.threshold::INT 
918                 AND 0 < max_longoverdue.threshold::INT THEN
919             new_sp_row.usr := match_user;
920             new_sp_row.org_unit := max_longoverdue.org_unit;
921             new_sp_row.standing_penalty := 35;
922             RETURN NEXT new_sp_row;
923         END IF;
924     END IF;
925
926
927     -- Start over for collections warning
928     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
929
930     -- Fail if the user has a collections-level fine balance
931     LOOP
932         tmp_grp := user_object.profile;
933         LOOP
934             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 4 AND org_unit = tmp_org.id;
935
936             IF max_fines.threshold IS NULL THEN
937                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
938             ELSE
939                 EXIT;
940             END IF;
941
942             IF tmp_grp IS NULL THEN
943                 EXIT;
944             END IF;
945         END LOOP;
946
947         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
948             EXIT;
949         END IF;
950
951         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
952
953     END LOOP;
954
955     IF max_fines.threshold IS NOT NULL THEN
956
957         RETURN QUERY
958             SELECT  *
959               FROM  actor.usr_standing_penalty
960               WHERE usr = match_user
961                     AND org_unit = max_fines.org_unit
962                     AND (stop_date IS NULL or stop_date > NOW())
963                     AND standing_penalty = 4;
964
965         SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( max_fines.org_unit );
966
967         SELECT  SUM(f.balance_owed) INTO current_fines
968           FROM  money.materialized_billable_xact_summary f
969                 JOIN (
970                     SELECT  r.id
971                       FROM  booking.reservation r
972                       WHERE r.usr = match_user
973                             AND r.pickup_lib IN (SELECT * FROM unnest(context_org_list))
974                             AND r.xact_finish IS NULL
975                                 UNION ALL
976                     SELECT  g.id
977                       FROM  money.grocery g
978                       WHERE g.usr = match_user
979                             AND g.billing_location IN (SELECT * FROM unnest(context_org_list))
980                             AND g.xact_finish IS NULL
981                                 UNION ALL
982                     SELECT  circ.id
983                       FROM  action.circulation circ
984                       WHERE circ.usr = match_user
985                             AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
986                             AND circ.xact_finish IS NULL ) l USING (id);
987
988         IF current_fines >= max_fines.threshold THEN
989             new_sp_row.usr := match_user;
990             new_sp_row.org_unit := max_fines.org_unit;
991             new_sp_row.standing_penalty := 4;
992             RETURN NEXT new_sp_row;
993         END IF;
994     END IF;
995
996     -- Start over for in collections
997     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
998
999     -- Remove the in-collections penalty if the user has paid down enough
1000     -- This penalty is different, because this code is not responsible for creating 
1001     -- new in-collections penalties, only for removing them
1002     LOOP
1003         tmp_grp := user_object.profile;
1004         LOOP
1005             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 30 AND org_unit = tmp_org.id;
1006
1007             IF max_fines.threshold IS NULL THEN
1008                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
1009             ELSE
1010                 EXIT;
1011             END IF;
1012
1013             IF tmp_grp IS NULL THEN
1014                 EXIT;
1015             END IF;
1016         END LOOP;
1017
1018         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
1019             EXIT;
1020         END IF;
1021
1022         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1023
1024     END LOOP;
1025
1026     IF max_fines.threshold IS NOT NULL THEN
1027
1028         SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( max_fines.org_unit );
1029
1030         -- first, see if the user had paid down to the threshold
1031         SELECT  SUM(f.balance_owed) INTO current_fines
1032           FROM  money.materialized_billable_xact_summary f
1033                 JOIN (
1034                     SELECT  r.id
1035                       FROM  booking.reservation r
1036                       WHERE r.usr = match_user
1037                             AND r.pickup_lib IN (SELECT * FROM unnest(context_org_list))
1038                             AND r.xact_finish IS NULL
1039                                 UNION ALL
1040                     SELECT  g.id
1041                       FROM  money.grocery g
1042                       WHERE g.usr = match_user
1043                             AND g.billing_location IN (SELECT * FROM unnest(context_org_list))
1044                             AND g.xact_finish IS NULL
1045                                 UNION ALL
1046                     SELECT  circ.id
1047                       FROM  action.circulation circ
1048                       WHERE circ.usr = match_user
1049                             AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
1050                             AND circ.xact_finish IS NULL ) l USING (id);
1051
1052         IF current_fines IS NULL OR current_fines <= max_fines.threshold THEN
1053             -- patron has paid down enough
1054
1055             SELECT INTO tmp_penalty * FROM config.standing_penalty WHERE id = 30;
1056
1057             IF tmp_penalty.org_depth IS NOT NULL THEN
1058
1059                 -- since this code is not responsible for applying the penalty, it can't 
1060                 -- guarantee the current context org will match the org at which the penalty 
1061                 --- was applied.  search up the org tree until we hit the configured penalty depth
1062                 SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
1063                 SELECT INTO tmp_depth depth FROM actor.org_unit_type WHERE id = tmp_org.ou_type;
1064
1065                 WHILE tmp_depth >= tmp_penalty.org_depth LOOP
1066
1067                     RETURN QUERY
1068                         SELECT  *
1069                           FROM  actor.usr_standing_penalty
1070                           WHERE usr = match_user
1071                                 AND org_unit = tmp_org.id
1072                                 AND (stop_date IS NULL or stop_date > NOW())
1073                                 AND standing_penalty = 30;
1074
1075                     IF tmp_org.parent_ou IS NULL THEN
1076                         EXIT;
1077                     END IF;
1078
1079                     SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
1080                     SELECT INTO tmp_depth depth FROM actor.org_unit_type WHERE id = tmp_org.ou_type;
1081                 END LOOP;
1082
1083             ELSE
1084
1085                 -- no penalty depth is defined, look for exact matches
1086
1087                 RETURN QUERY
1088                     SELECT  *
1089                       FROM  actor.usr_standing_penalty
1090                       WHERE usr = match_user
1091                             AND org_unit = max_fines.org_unit
1092                             AND (stop_date IS NULL or stop_date > NOW())
1093                             AND standing_penalty = 30;
1094             END IF;
1095     
1096         END IF;
1097
1098     END IF;
1099
1100     RETURN;
1101 END;
1102 $func$ LANGUAGE plpgsql;
1103
1104
1105 -- from 110.hold_matrix.sql
1106
1107 CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT, retargetting BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
1108 DECLARE
1109     matchpoint_id        INT;
1110     user_object        actor.usr%ROWTYPE;
1111     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
1112     standing_penalty    config.standing_penalty%ROWTYPE;
1113     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
1114     transit_source        actor.org_unit%ROWTYPE;
1115     item_object        asset.copy%ROWTYPE;
1116     item_cn_object     asset.call_number%ROWTYPE;
1117     item_status_object  config.copy_status%ROWTYPE;
1118     item_location_object    asset.copy_location%ROWTYPE;
1119     ou_skip              actor.org_unit_setting%ROWTYPE;
1120     result            action.matrix_test_result;
1121     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
1122     use_active_date   TEXT;
1123     age_protect_date  TIMESTAMP WITH TIME ZONE;
1124     hold_count        INT;
1125     hold_transit_prox    INT;
1126     frozen_hold_count    INT;
1127     context_org_list    INT[];
1128     done            BOOL := FALSE;
1129     hold_penalty TEXT;
1130 BEGIN
1131     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
1132     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( pickup_ou );
1133
1134     result.success := TRUE;
1135
1136     -- The HOLD penalty block only applies to new holds.
1137     -- The CAPTURE penalty block applies to existing holds.
1138     hold_penalty := 'HOLD';
1139     IF retargetting THEN
1140         hold_penalty := 'CAPTURE';
1141     END IF;
1142
1143     -- Fail if we couldn't find a user
1144     IF user_object.id IS NULL THEN
1145         result.fail_part := 'no_user';
1146         result.success := FALSE;
1147         done := TRUE;
1148         RETURN NEXT result;
1149         RETURN;
1150     END IF;
1151
1152     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
1153
1154     -- Fail if we couldn't find a copy
1155     IF item_object.id IS NULL THEN
1156         result.fail_part := 'no_item';
1157         result.success := FALSE;
1158         done := TRUE;
1159         RETURN NEXT result;
1160         RETURN;
1161     END IF;
1162
1163     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
1164     result.matchpoint := matchpoint_id;
1165
1166     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
1167
1168     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
1169     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
1170         result.fail_part := 'circ.holds.target_skip_me';
1171         result.success := FALSE;
1172         done := TRUE;
1173         RETURN NEXT result;
1174         RETURN;
1175     END IF;
1176
1177     -- Fail if user is barred
1178     IF user_object.barred IS TRUE THEN
1179         result.fail_part := 'actor.usr.barred';
1180         result.success := FALSE;
1181         done := TRUE;
1182         RETURN NEXT result;
1183         RETURN;
1184     END IF;
1185
1186     SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
1187     SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
1188     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
1189
1190     -- Fail if we couldn't find any matchpoint (requires a default)
1191     IF matchpoint_id IS NULL THEN
1192         result.fail_part := 'no_matchpoint';
1193         result.success := FALSE;
1194         done := TRUE;
1195         RETURN NEXT result;
1196         RETURN;
1197     END IF;
1198
1199     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
1200
1201     IF hold_test.holdable IS FALSE THEN
1202         result.fail_part := 'config.hold_matrix_test.holdable';
1203         result.success := FALSE;
1204         done := TRUE;
1205         RETURN NEXT result;
1206     END IF;
1207
1208     IF item_object.holdable IS FALSE THEN
1209         result.fail_part := 'item.holdable';
1210         result.success := FALSE;
1211         done := TRUE;
1212         RETURN NEXT result;
1213     END IF;
1214
1215     IF item_status_object.holdable IS FALSE THEN
1216         result.fail_part := 'status.holdable';
1217         result.success := FALSE;
1218         done := TRUE;
1219         RETURN NEXT result;
1220     END IF;
1221
1222     IF item_location_object.holdable IS FALSE THEN
1223         result.fail_part := 'location.holdable';
1224         result.success := FALSE;
1225         done := TRUE;
1226         RETURN NEXT result;
1227     END IF;
1228
1229     IF hold_test.transit_range IS NOT NULL THEN
1230         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
1231         IF hold_test.distance_is_from_owner THEN
1232             SELECT INTO transit_source ou.* FROM actor.org_unit ou JOIN asset.call_number cn ON (cn.owning_lib = ou.id) WHERE cn.id = item_object.call_number;
1233         ELSE
1234             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
1235         END IF;
1236
1237         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
1238
1239         IF NOT FOUND THEN
1240             result.fail_part := 'transit_range';
1241             result.success := FALSE;
1242             done := TRUE;
1243             RETURN NEXT result;
1244         END IF;
1245     END IF;
1246  
1247     FOR standing_penalty IN
1248         SELECT  DISTINCT csp.*
1249           FROM  actor.usr_standing_penalty usp
1250                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1251           WHERE usr = match_user
1252                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1253                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1254                 AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
1255
1256         result.fail_part := standing_penalty.name;
1257         result.success := FALSE;
1258         done := TRUE;
1259         RETURN NEXT result;
1260     END LOOP;
1261
1262     IF hold_test.stop_blocked_user IS TRUE THEN
1263         FOR standing_penalty IN
1264             SELECT  DISTINCT csp.*
1265               FROM  actor.usr_standing_penalty usp
1266                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1267               WHERE usr = match_user
1268                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1269                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1270                     AND csp.block_list LIKE '%CIRC%' LOOP
1271     
1272             result.fail_part := standing_penalty.name;
1273             result.success := FALSE;
1274             done := TRUE;
1275             RETURN NEXT result;
1276         END LOOP;
1277     END IF;
1278
1279     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
1280         SELECT    INTO hold_count COUNT(*)
1281           FROM    action.hold_request
1282           WHERE    usr = match_user
1283             AND fulfillment_time IS NULL
1284             AND cancel_time IS NULL
1285             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
1286
1287         IF hold_count >= hold_test.max_holds THEN
1288             result.fail_part := 'config.hold_matrix_test.max_holds';
1289             result.success := FALSE;
1290             done := TRUE;
1291             RETURN NEXT result;
1292         END IF;
1293     END IF;
1294
1295     IF item_object.age_protect IS NOT NULL THEN
1296         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
1297         IF hold_test.distance_is_from_owner THEN
1298             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
1299         ELSE
1300             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
1301         END IF;
1302         IF use_active_date = 'true' THEN
1303             age_protect_date := COALESCE(item_object.active_date, NOW());
1304         ELSE
1305             age_protect_date := item_object.create_date;
1306         END IF;
1307         IF age_protect_date + age_protect_object.age > NOW() THEN
1308             IF hold_test.distance_is_from_owner THEN
1309                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
1310                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
1311             ELSE
1312                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
1313             END IF;
1314
1315             IF hold_transit_prox > age_protect_object.prox THEN
1316                 result.fail_part := 'config.rule_age_hold_protect.prox';
1317                 result.success := FALSE;
1318                 done := TRUE;
1319                 RETURN NEXT result;
1320             END IF;
1321         END IF;
1322     END IF;
1323
1324     IF NOT done THEN
1325         RETURN NEXT result;
1326     END IF;
1327
1328     RETURN;
1329 END;
1330 $func$ LANGUAGE plpgsql;
1331
1332
1333 -- from 300.schema.staged_search.sql
1334
1335 CREATE OR REPLACE FUNCTION search.query_parser_fts (
1336
1337     param_search_ou INT,
1338     param_depth     INT,
1339     param_query     TEXT,
1340     param_statuses  INT[],
1341     param_locations INT[],
1342     param_offset    INT,
1343     param_check     INT,
1344     param_limit     INT,
1345     metarecord      BOOL,
1346     staff           BOOL,
1347     deleted_search  BOOL,
1348     param_pref_ou   INT DEFAULT NULL
1349 ) RETURNS SETOF search.search_result AS $func$
1350 DECLARE
1351
1352     current_res         search.search_result%ROWTYPE;
1353     search_org_list     INT[];
1354     luri_org_list       INT[];
1355     tmp_int_list        INT[];
1356
1357     check_limit         INT;
1358     core_limit          INT;
1359     core_offset         INT;
1360     tmp_int             INT;
1361
1362     core_result         RECORD;
1363     core_cursor         REFCURSOR;
1364     core_rel_query      TEXT;
1365
1366     total_count         INT := 0;
1367     check_count         INT := 0;
1368     deleted_count       INT := 0;
1369     visible_count       INT := 0;
1370     excluded_count      INT := 0;
1371
1372 BEGIN
1373
1374     check_limit := COALESCE( param_check, 1000 );
1375     core_limit  := COALESCE( param_limit, 25000 );
1376     core_offset := COALESCE( param_offset, 0 );
1377
1378     -- core_skip_chk := COALESCE( param_skip_chk, 1 );
1379
1380     IF param_search_ou > 0 THEN
1381         IF param_depth IS NOT NULL THEN
1382             SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
1383         ELSE
1384             SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
1385         END IF;
1386
1387         SELECT ARRAY_AGG(distinct id) INTO luri_org_list FROM actor.org_unit_ancestors( param_search_ou );
1388
1389     ELSIF param_search_ou < 0 THEN
1390         SELECT ARRAY_AGG(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
1391
1392         FOR tmp_int IN SELECT * FROM UNNEST(search_org_list) LOOP
1393             SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( tmp_int );
1394             luri_org_list := luri_org_list || tmp_int_list;
1395         END LOOP;
1396
1397         SELECT ARRAY_AGG(DISTINCT x.id) INTO luri_org_list FROM UNNEST(luri_org_list) x(id);
1398
1399     ELSIF param_search_ou = 0 THEN
1400         -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
1401     END IF;
1402
1403     IF param_pref_ou IS NOT NULL THEN
1404         SELECT array_agg(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors(param_pref_ou);
1405         luri_org_list := luri_org_list || tmp_int_list;
1406     END IF;
1407
1408     OPEN core_cursor FOR EXECUTE param_query;
1409
1410     LOOP
1411
1412         FETCH core_cursor INTO core_result;
1413         EXIT WHEN NOT FOUND;
1414         EXIT WHEN total_count >= core_limit;
1415
1416         total_count := total_count + 1;
1417
1418         CONTINUE WHEN total_count NOT BETWEEN  core_offset + 1 AND check_limit + core_offset;
1419
1420         check_count := check_count + 1;
1421
1422         IF NOT deleted_search THEN
1423
1424             PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
1425             IF NOT FOUND THEN
1426                 -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
1427                 deleted_count := deleted_count + 1;
1428                 CONTINUE;
1429             END IF;
1430
1431             PERFORM 1
1432               FROM  biblio.record_entry b
1433                     JOIN config.bib_source s ON (b.source = s.id)
1434               WHERE s.transcendant
1435                     AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
1436
1437             IF FOUND THEN
1438                 -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
1439                 visible_count := visible_count + 1;
1440
1441                 current_res.id = core_result.id;
1442                 current_res.rel = core_result.rel;
1443
1444                 tmp_int := 1;
1445                 IF metarecord THEN
1446                     SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
1447                 END IF;
1448
1449                 IF tmp_int = 1 THEN
1450                     current_res.record = core_result.records[1];
1451                 ELSE
1452                     current_res.record = NULL;
1453                 END IF;
1454
1455                 RETURN NEXT current_res;
1456
1457                 CONTINUE;
1458             END IF;
1459
1460             PERFORM 1
1461               FROM  asset.call_number cn
1462                     JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
1463                     JOIN asset.uri uri ON (map.uri = uri.id)
1464               WHERE NOT cn.deleted
1465                     AND cn.label = '##URI##'
1466                     AND uri.active
1467                     AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
1468                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
1469                     AND cn.owning_lib IN ( SELECT * FROM unnest( luri_org_list ) )
1470               LIMIT 1;
1471
1472             IF FOUND THEN
1473                 -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
1474                 visible_count := visible_count + 1;
1475
1476                 current_res.id = core_result.id;
1477                 current_res.rel = core_result.rel;
1478
1479                 tmp_int := 1;
1480                 IF metarecord THEN
1481                     SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
1482                 END IF;
1483
1484                 IF tmp_int = 1 THEN
1485                     current_res.record = core_result.records[1];
1486                 ELSE
1487                     current_res.record = NULL;
1488                 END IF;
1489
1490                 RETURN NEXT current_res;
1491
1492                 CONTINUE;
1493             END IF;
1494
1495             IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
1496
1497                 PERFORM 1
1498                   FROM  asset.call_number cn
1499                         JOIN asset.copy cp ON (cp.call_number = cn.id)
1500                   WHERE NOT cn.deleted
1501                         AND NOT cp.deleted
1502                         AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
1503                         AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
1504                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
1505                   LIMIT 1;
1506
1507                 IF NOT FOUND THEN
1508                     PERFORM 1
1509                       FROM  biblio.peer_bib_copy_map pr
1510                             JOIN asset.copy cp ON (cp.id = pr.target_copy)
1511                       WHERE NOT cp.deleted
1512                             AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
1513                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
1514                             AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
1515                       LIMIT 1;
1516
1517                     IF NOT FOUND THEN
1518                     -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
1519                         excluded_count := excluded_count + 1;
1520                         CONTINUE;
1521                     END IF;
1522                 END IF;
1523
1524             END IF;
1525
1526             IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
1527
1528                 PERFORM 1
1529                   FROM  asset.call_number cn
1530                         JOIN asset.copy cp ON (cp.call_number = cn.id)
1531                   WHERE NOT cn.deleted
1532                         AND NOT cp.deleted
1533                         AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
1534                         AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
1535                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
1536                   LIMIT 1;
1537
1538                 IF NOT FOUND THEN
1539                     PERFORM 1
1540                       FROM  biblio.peer_bib_copy_map pr
1541                             JOIN asset.copy cp ON (cp.id = pr.target_copy)
1542                       WHERE NOT cp.deleted
1543                             AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
1544                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
1545                             AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
1546                       LIMIT 1;
1547
1548                     IF NOT FOUND THEN
1549                         -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
1550                         excluded_count := excluded_count + 1;
1551                         CONTINUE;
1552                     END IF;
1553                 END IF;
1554
1555             END IF;
1556
1557             IF staff IS NULL OR NOT staff THEN
1558
1559                 PERFORM 1
1560                   FROM  asset.opac_visible_copies
1561                   WHERE circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
1562                         AND record IN ( SELECT * FROM unnest( core_result.records ) )
1563                   LIMIT 1;
1564
1565                 IF NOT FOUND THEN
1566                     PERFORM 1
1567                       FROM  biblio.peer_bib_copy_map pr
1568                             JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
1569                       WHERE cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
1570                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
1571                       LIMIT 1;
1572
1573                     IF NOT FOUND THEN
1574
1575                         -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
1576                         excluded_count := excluded_count + 1;
1577                         CONTINUE;
1578                     END IF;
1579                 END IF;
1580
1581             ELSE
1582
1583                 PERFORM 1
1584                   FROM  asset.call_number cn
1585                         JOIN asset.copy cp ON (cp.call_number = cn.id)
1586                   WHERE NOT cn.deleted
1587                         AND NOT cp.deleted
1588                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
1589                         AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
1590                   LIMIT 1;
1591
1592                 IF NOT FOUND THEN
1593
1594                     PERFORM 1
1595                       FROM  biblio.peer_bib_copy_map pr
1596                             JOIN asset.copy cp ON (cp.id = pr.target_copy)
1597                       WHERE NOT cp.deleted
1598                             AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
1599                             AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
1600                       LIMIT 1;
1601
1602                     IF NOT FOUND THEN
1603
1604                         PERFORM 1
1605                           FROM  asset.call_number cn
1606                                 JOIN asset.copy cp ON (cp.call_number = cn.id)
1607                           WHERE cn.record IN ( SELECT * FROM unnest( core_result.records ) )
1608                                 AND NOT cp.deleted
1609                           LIMIT 1;
1610
1611                         IF FOUND THEN
1612                             -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
1613                             excluded_count := excluded_count + 1;
1614                             CONTINUE;
1615                         END IF;
1616                     END IF;
1617
1618                 END IF;
1619
1620             END IF;
1621
1622         END IF;
1623
1624         visible_count := visible_count + 1;
1625
1626         current_res.id = core_result.id;
1627         current_res.rel = core_result.rel;
1628
1629         tmp_int := 1;
1630         IF metarecord THEN
1631             SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
1632         END IF;
1633
1634         IF tmp_int = 1 THEN
1635             current_res.record = core_result.records[1];
1636         ELSE
1637             current_res.record = NULL;
1638         END IF;
1639
1640         RETURN NEXT current_res;
1641
1642         IF visible_count % 1000 = 0 THEN
1643             -- RAISE NOTICE ' % visible so far ... ', visible_count;
1644         END IF;
1645
1646     END LOOP;
1647
1648     current_res.id = NULL;
1649     current_res.rel = NULL;
1650     current_res.record = NULL;
1651     current_res.total = total_count;
1652     current_res.checked = check_count;
1653     current_res.deleted = deleted_count;
1654     current_res.visible = visible_count;
1655     current_res.excluded = excluded_count;
1656
1657     CLOSE core_cursor;
1658
1659     RETURN NEXT current_res;
1660
1661 END;
1662 $func$ LANGUAGE PLPGSQL;
1663
1664
1665 -- from 990.schema.unapi.sql
1666
1667 CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT)
1668 RETURNS anyarray AS $$
1669     SELECT ARRAY_AGG(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2;
1670 $$ LANGUAGE SQL STABLE;
1671
1672
1673 -- from 999.functions.global.sql
1674
1675 CREATE OR REPLACE FUNCTION asset.merge_record_assets( target_record BIGINT, source_record BIGINT ) RETURNS INT AS $func$
1676 DECLARE
1677     moved_objects INT := 0;
1678     source_cn     asset.call_number%ROWTYPE;
1679     target_cn     asset.call_number%ROWTYPE;
1680     metarec       metabib.metarecord%ROWTYPE;
1681     hold          action.hold_request%ROWTYPE;
1682     ser_rec       serial.record_entry%ROWTYPE;
1683     ser_sub       serial.subscription%ROWTYPE;
1684     acq_lineitem  acq.lineitem%ROWTYPE;
1685     acq_request   acq.user_request%ROWTYPE;
1686     booking       booking.resource_type%ROWTYPE;
1687     source_part   biblio.monograph_part%ROWTYPE;
1688     target_part   biblio.monograph_part%ROWTYPE;
1689     multi_home    biblio.peer_bib_copy_map%ROWTYPE;
1690     uri_count     INT := 0;
1691     counter       INT := 0;
1692     uri_datafield TEXT;
1693     uri_text      TEXT := '';
1694 BEGIN
1695
1696     -- move any 856 entries on records that have at least one MARC-mapped URI entry
1697     SELECT  INTO uri_count COUNT(*)
1698       FROM  asset.uri_call_number_map m
1699             JOIN asset.call_number cn ON (m.call_number = cn.id)
1700       WHERE cn.record = source_record;
1701
1702     IF uri_count > 0 THEN
1703         
1704         -- This returns more nodes than you might expect:
1705         -- 7 instead of 1 for an 856 with $u $y $9
1706         SELECT  COUNT(*) INTO counter
1707           FROM  oils_xpath_table(
1708                     'id',
1709                     'marc',
1710                     'biblio.record_entry',
1711                     '//*[@tag="856"]',
1712                     'id=' || source_record
1713                 ) as t(i int,c text);
1714     
1715         FOR i IN 1 .. counter LOOP
1716             SELECT  '<datafield xmlns="http://www.loc.gov/MARC21/slim"' || 
1717                         ' tag="856"' ||
1718                         ' ind1="' || FIRST(ind1) || '"'  ||
1719                         ' ind2="' || FIRST(ind2) || '">' ||
1720                         STRING_AGG(
1721                             '<subfield code="' || subfield || '">' ||
1722                             regexp_replace(
1723                                 regexp_replace(
1724                                     regexp_replace(data,'&','&amp;','g'),
1725                                     '>', '&gt;', 'g'
1726                                 ),
1727                                 '<', '&lt;', 'g'
1728                             ) || '</subfield>', ''
1729                         ) || '</datafield>' INTO uri_datafield
1730               FROM  oils_xpath_table(
1731                         'id',
1732                         'marc',
1733                         'biblio.record_entry',
1734                         '//*[@tag="856"][position()=' || i || ']/@ind1|' ||
1735                         '//*[@tag="856"][position()=' || i || ']/@ind2|' ||
1736                         '//*[@tag="856"][position()=' || i || ']/*/@code|' ||
1737                         '//*[@tag="856"][position()=' || i || ']/*[@code]',
1738                         'id=' || source_record
1739                     ) as t(id int,ind1 text, ind2 text,subfield text,data text);
1740
1741             -- As most of the results will be NULL, protect against NULLifying
1742             -- the valid content that we do generate
1743             uri_text := uri_text || COALESCE(uri_datafield, '');
1744         END LOOP;
1745
1746         IF uri_text <> '' THEN
1747             UPDATE  biblio.record_entry
1748               SET   marc = regexp_replace(marc,'(</[^>]*record>)', uri_text || E'\\1')
1749               WHERE id = target_record;
1750         END IF;
1751
1752     END IF;
1753
1754         -- Find and move metarecords to the target record
1755         SELECT  INTO metarec *
1756           FROM  metabib.metarecord
1757           WHERE master_record = source_record;
1758
1759         IF FOUND THEN
1760                 UPDATE  metabib.metarecord
1761                   SET   master_record = target_record,
1762                         mods = NULL
1763                   WHERE id = metarec.id;
1764
1765                 moved_objects := moved_objects + 1;
1766         END IF;
1767
1768         -- Find call numbers attached to the source ...
1769         FOR source_cn IN SELECT * FROM asset.call_number WHERE record = source_record LOOP
1770
1771                 SELECT  INTO target_cn *
1772                   FROM  asset.call_number
1773                   WHERE label = source_cn.label
1774                         AND owning_lib = source_cn.owning_lib
1775                         AND record = target_record
1776                         AND NOT deleted;
1777
1778                 -- ... and if there's a conflicting one on the target ...
1779                 IF FOUND THEN
1780
1781                         -- ... move the copies to that, and ...
1782                         UPDATE  asset.copy
1783                           SET   call_number = target_cn.id
1784                           WHERE call_number = source_cn.id;
1785
1786                         -- ... move V holds to the move-target call number
1787                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_cn.id AND hold_type = 'V' LOOP
1788                 
1789                                 UPDATE  action.hold_request
1790                                   SET   target = target_cn.id
1791                                   WHERE id = hold.id;
1792                 
1793                                 moved_objects := moved_objects + 1;
1794                         END LOOP;
1795
1796                 -- ... if not ...
1797                 ELSE
1798                         -- ... just move the call number to the target record
1799                         UPDATE  asset.call_number
1800                           SET   record = target_record
1801                           WHERE id = source_cn.id;
1802                 END IF;
1803
1804                 moved_objects := moved_objects + 1;
1805         END LOOP;
1806
1807         -- Find T holds targeting the source record ...
1808         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_record AND hold_type = 'T' LOOP
1809
1810                 -- ... and move them to the target record
1811                 UPDATE  action.hold_request
1812                   SET   target = target_record
1813                   WHERE id = hold.id;
1814
1815                 moved_objects := moved_objects + 1;
1816         END LOOP;
1817
1818         -- Find serial records targeting the source record ...
1819         FOR ser_rec IN SELECT * FROM serial.record_entry WHERE record = source_record LOOP
1820                 -- ... and move them to the target record
1821                 UPDATE  serial.record_entry
1822                   SET   record = target_record
1823                   WHERE id = ser_rec.id;
1824
1825                 moved_objects := moved_objects + 1;
1826         END LOOP;
1827
1828         -- Find serial subscriptions targeting the source record ...
1829         FOR ser_sub IN SELECT * FROM serial.subscription WHERE record_entry = source_record LOOP
1830                 -- ... and move them to the target record
1831                 UPDATE  serial.subscription
1832                   SET   record_entry = target_record
1833                   WHERE id = ser_sub.id;
1834
1835                 moved_objects := moved_objects + 1;
1836         END LOOP;
1837
1838         -- Find booking resource types targeting the source record ...
1839         FOR booking IN SELECT * FROM booking.resource_type WHERE record = source_record LOOP
1840                 -- ... and move them to the target record
1841                 UPDATE  booking.resource_type
1842                   SET   record = target_record
1843                   WHERE id = booking.id;
1844
1845                 moved_objects := moved_objects + 1;
1846         END LOOP;
1847
1848         -- Find acq lineitems targeting the source record ...
1849         FOR acq_lineitem IN SELECT * FROM acq.lineitem WHERE eg_bib_id = source_record LOOP
1850                 -- ... and move them to the target record
1851                 UPDATE  acq.lineitem
1852                   SET   eg_bib_id = target_record
1853                   WHERE id = acq_lineitem.id;
1854
1855                 moved_objects := moved_objects + 1;
1856         END LOOP;
1857
1858         -- Find acq user purchase requests targeting the source record ...
1859         FOR acq_request IN SELECT * FROM acq.user_request WHERE eg_bib = source_record LOOP
1860                 -- ... and move them to the target record
1861                 UPDATE  acq.user_request
1862                   SET   eg_bib = target_record
1863                   WHERE id = acq_request.id;
1864
1865                 moved_objects := moved_objects + 1;
1866         END LOOP;
1867
1868         -- Find parts attached to the source ...
1869         FOR source_part IN SELECT * FROM biblio.monograph_part WHERE record = source_record LOOP
1870
1871                 SELECT  INTO target_part *
1872                   FROM  biblio.monograph_part
1873                   WHERE label = source_part.label
1874                         AND record = target_record;
1875
1876                 -- ... and if there's a conflicting one on the target ...
1877                 IF FOUND THEN
1878
1879                         -- ... move the copy-part maps to that, and ...
1880                         UPDATE  asset.copy_part_map
1881                           SET   part = target_part.id
1882                           WHERE part = source_part.id;
1883
1884                         -- ... move P holds to the move-target part
1885                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_part.id AND hold_type = 'P' LOOP
1886                 
1887                                 UPDATE  action.hold_request
1888                                   SET   target = target_part.id
1889                                   WHERE id = hold.id;
1890                 
1891                                 moved_objects := moved_objects + 1;
1892                         END LOOP;
1893
1894                 -- ... if not ...
1895                 ELSE
1896                         -- ... just move the part to the target record
1897                         UPDATE  biblio.monograph_part
1898                           SET   record = target_record
1899                           WHERE id = source_part.id;
1900                 END IF;
1901
1902                 moved_objects := moved_objects + 1;
1903         END LOOP;
1904
1905         -- Find multi_home items attached to the source ...
1906         FOR multi_home IN SELECT * FROM biblio.peer_bib_copy_map WHERE peer_record = source_record LOOP
1907                 -- ... and move them to the target record
1908                 UPDATE  biblio.peer_bib_copy_map
1909                   SET   peer_record = target_record
1910                   WHERE id = multi_home.id;
1911
1912                 moved_objects := moved_objects + 1;
1913         END LOOP;
1914
1915         -- And delete mappings where the item's home bib was merged with the peer bib
1916         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = (
1917                 SELECT (SELECT record FROM asset.call_number WHERE id = call_number)
1918                 FROM asset.copy WHERE id = target_copy
1919         );
1920
1921     -- Finally, "delete" the source record
1922     DELETE FROM biblio.record_entry WHERE id = source_record;
1923
1924         -- That's all, folks!
1925         RETURN moved_objects;
1926 END;
1927 $func$ LANGUAGE plpgsql;
1928
1929 -- from reporter-schema.sql
1930
1931 CREATE OR REPLACE VIEW reporter.simple_record AS
1932 SELECT  r.id,
1933         s.metarecord,
1934         r.fingerprint,
1935         r.quality,
1936         r.tcn_source,
1937         r.tcn_value,
1938         title.value AS title,
1939         uniform_title.value AS uniform_title,
1940         author.value AS author,
1941         publisher.value AS publisher,
1942         SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
1943         series_title.value AS series_title,
1944         series_statement.value AS series_statement,
1945         summary.value AS summary,
1946         ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
1947         ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
1948         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
1949         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
1950         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
1951         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
1952         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
1953         ARRAY((SELECT value FROM metabib.full_rec WHERE tag = '856' AND subfield IN ('3','y','u') AND record = r.id ORDER BY CASE WHEN subfield IN ('3','y') THEN 0 ELSE 1 END)) AS external_uri
1954   FROM  biblio.record_entry r
1955         JOIN metabib.metarecord_source_map s ON (s.source = r.id)
1956         LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
1957         LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
1958         LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
1959         LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
1960         LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
1961         LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
1962         LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
1963         LEFT JOIN metabib.full_rec series_title ON (r.id = series_title.record AND series_title.tag IN ('830','440') AND series_title.subfield = 'a')
1964         LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
1965         LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
1966   GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
1967
1968 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
1969 SELECT  r.id,
1970     r.fingerprint,
1971     r.quality,
1972     r.tcn_source,
1973     r.tcn_value,
1974     FIRST(title.value) AS title,
1975     FIRST(author.value) AS author,
1976     STRING_AGG(DISTINCT publisher.value, ', ') AS publisher,
1977     STRING_AGG(DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$), ', ') AS pubdate,
1978     CASE WHEN ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) = '{NULL}'
1979         THEN NULL
1980         ELSE ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') )
1981     END AS isbn,
1982     CASE WHEN ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) = '{NULL}'
1983         THEN NULL
1984         ELSE ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') )
1985     END AS issn
1986   FROM  biblio.record_entry r
1987     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
1988     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
1989     LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND (publisher.tag = '260' OR (publisher.tag = '264' AND publisher.ind2 = '1')) AND publisher.subfield = 'b')
1990     LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND (pubdate.tag = '260' OR (pubdate.tag = '264' AND pubdate.ind2 = '1')) AND pubdate.subfield = 'c')
1991     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
1992     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
1993   GROUP BY 1,2,3,4,5;
1994
1995 COMMIT;
1996
1997 -- Not running changes from example.reporter-extension.sql since these are
1998 -- not installed by default, but including a helpful note.
1999
2000 \qecho 'There were also changes in example.reporter-extension.sql'
2001 \qecho 'Please run that script again if you use it in your system'
2002 \qecho 'to apply new changes.'
2003