Translation updates - po files
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.12.5-3.0-rc-upgrade-db.sql
1 --Upgrade Script for 2.12.5 to 3.0-rc
2 \set eg_version '''3.0-rc'''
3
4 -- verify that we're running a recent enough version of Pg
5 \set ON_ERROR_STOP on
6 BEGIN;
7
8 DO $$
9    DECLARE ver INTEGER;
10    BEGIN
11       SELECT current_setting('server_version_num') INTO ver;
12       IF (ver < 90400) THEN
13          RAISE EXCEPTION 'Not running a new enough version of PostgreSQL. Minimum required is 9.4; you have %', ver;
14       END IF;
15    END;
16 $$;
17
18 COMMIT;
19 \set ON_ERROR_STOP off
20
21 BEGIN;
22 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.0-beta1', :eg_version);
23
24 SELECT evergreen.upgrade_deps_block_check('1032', :eg_version); -- Bmagic/csharp/gmcharlt
25
26 CREATE OR REPLACE VIEW action.all_circulation_combined_types AS 
27  SELECT acirc.id AS id,
28     acirc.xact_start,
29     acirc.circ_lib,
30     acirc.circ_staff,
31     acirc.create_time,
32     ac_acirc.circ_modifier AS item_type,
33     'regular_circ'::text AS circ_type
34    FROM action.circulation acirc,
35     asset.copy ac_acirc
36   WHERE acirc.target_copy = ac_acirc.id
37 UNION ALL
38  SELECT ancc.id::BIGINT AS id,
39     ancc.circ_time AS xact_start,
40     ancc.circ_lib,
41     ancc.staff AS circ_staff,
42     ancc.circ_time AS create_time,
43     cnct_ancc.name AS item_type,
44     'non-cat_circ'::text AS circ_type
45    FROM action.non_cataloged_circulation ancc,
46     config.non_cataloged_type cnct_ancc
47   WHERE ancc.item_type = cnct_ancc.id
48 UNION ALL
49  SELECT aihu.id::BIGINT AS id,
50     aihu.use_time AS xact_start,
51     aihu.org_unit AS circ_lib,
52     aihu.staff AS circ_staff,
53     aihu.use_time AS create_time,
54     ac_aihu.circ_modifier AS item_type,
55     'in-house_use'::text AS circ_type
56    FROM action.in_house_use aihu,
57     asset.copy ac_aihu
58   WHERE aihu.item = ac_aihu.id
59 UNION ALL
60  SELECT ancihu.id::BIGINT AS id,
61     ancihu.use_time AS xact_start,
62     ancihu.org_unit AS circ_lib,
63     ancihu.staff AS circ_staff,
64     ancihu.use_time AS create_time,
65     cnct_ancihu.name AS item_type,
66     'non-cat_circ'::text AS circ_type
67    FROM action.non_cat_in_house_use ancihu,
68     config.non_cataloged_type cnct_ancihu
69   WHERE ancihu.item_type = cnct_ancihu.id
70 UNION ALL
71  SELECT aacirc.id AS id,
72     aacirc.xact_start,
73     aacirc.circ_lib,
74     aacirc.circ_staff,
75     aacirc.create_time,
76     ac_aacirc.circ_modifier AS item_type,
77     'aged_circ'::text AS circ_type
78    FROM action.aged_circulation aacirc,
79     asset.copy ac_aacirc
80   WHERE aacirc.target_copy = ac_aacirc.id;
81
82
83 SELECT evergreen.upgrade_deps_block_check('1034', :eg_version);
84
85 ALTER TABLE config.hold_matrix_matchpoint
86     ADD COLUMN description TEXT;
87
88 ALTER TABLE config.circ_matrix_matchpoint
89     ADD COLUMN description TEXT;
90
91
92 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1035', :eg_version); -- dyrcona/gmcharlt
93
94 -- Per Lp bug 1684984, the config.internal_flag,
95 -- ingest.disable_metabib_field_entry, was made obsolete by the
96 -- addition of the ingest.skip_browse_indexing,
97 -- ingest.skip_search_indexing, and ingest.skip_facet_indexing flags.
98 -- Since it is not used in the database, we delete it.
99 DELETE FROM config.internal_flag
100 WHERE name = 'ingest.disable_metabib_field_entry';
101
102
103 SELECT evergreen.upgrade_deps_block_check('1036', :eg_version);
104
105 CREATE OR REPLACE FUNCTION config.update_hard_due_dates () RETURNS INT AS $func$
106 DECLARE
107     temp_value  config.hard_due_date_values%ROWTYPE;
108     updated     INT := 0;
109 BEGIN
110     FOR temp_value IN
111       SELECT  DISTINCT ON (hard_due_date) *
112         FROM  config.hard_due_date_values
113         WHERE active_date <= NOW() -- We've passed (or are at) the rollover time
114         ORDER BY hard_due_date, active_date DESC -- Latest (nearest to us) active time
115    LOOP
116         UPDATE  config.hard_due_date
117           SET   ceiling_date = temp_value.ceiling_date
118           WHERE id = temp_value.hard_due_date
119                 AND ceiling_date <> temp_value.ceiling_date -- Time is equal if we've already updated the chdd
120                 AND temp_value.ceiling_date >= NOW(); -- Don't update ceiling dates to the past
121
122         IF FOUND THEN
123             updated := updated + 1;
124         END IF;
125     END LOOP;
126
127     RETURN updated;
128 END;
129 $func$ LANGUAGE plpgsql;
130
131
132 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1041', :eg_version); -- stompro/csharp/gmcharlt
133
134 --delete all instances from permission.grp_perm_map first
135 DELETE FROM permission.grp_perm_map where perm in 
136 (select id from permission.perm_list where code='SET_CIRC_MISSING');
137
138 --delete all instances from permission.usr_perm_map too
139 DELETE FROM permission.usr_perm_map where perm in
140 (select id from permission.perm_list where code='SET_CIRC_MISSING');
141
142 --delete from permission.perm_list
143 DELETE FROM permission.perm_list where code='SET_CIRC_MISSING';
144
145
146 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1042', :eg_version); -- mmorgan/gmcharlt
147
148 ALTER TABLE asset.copy_location
149           ADD COLUMN url TEXT;
150
151
152 SELECT evergreen.upgrade_deps_block_check('1043', :eg_version);
153
154 ALTER TABLE action_trigger.event_definition
155     ADD COLUMN retention_interval INTERVAL;
156
157 CREATE OR REPLACE FUNCTION action_trigger.check_valid_retention_interval() 
158     RETURNS TRIGGER AS $_$
159 BEGIN
160
161     /*
162      * 1. Retention intervals are alwyas allowed on active hooks.
163      * 2. On passive hooks, retention intervals are only allowed
164      *    when the event definition has a max_delay value and the
165      *    retention_interval value is greater than the difference 
166      *    beteween the delay and max_delay values.
167      */ 
168     PERFORM TRUE FROM action_trigger.hook 
169         WHERE key = NEW.hook AND NOT passive;
170
171     IF FOUND THEN
172         RETURN NEW;
173     END IF;
174
175     IF NEW.max_delay IS NOT NULL THEN
176         IF EXTRACT(EPOCH FROM NEW.retention_interval) > 
177             ABS(EXTRACT(EPOCH FROM (NEW.max_delay - NEW.delay))) THEN
178             RETURN NEW; -- all good
179         ELSE
180             RAISE EXCEPTION 'retention_interval is too short';
181         END IF;
182     ELSE
183         RAISE EXCEPTION 'retention_interval requires max_delay';
184     END IF;
185 END;
186 $_$ LANGUAGE PLPGSQL;
187
188 CREATE TRIGGER is_valid_retention_interval 
189     BEFORE INSERT OR UPDATE ON action_trigger.event_definition
190     FOR EACH ROW WHEN (NEW.retention_interval IS NOT NULL)
191     EXECUTE PROCEDURE action_trigger.check_valid_retention_interval();
192
193 CREATE OR REPLACE FUNCTION action_trigger.purge_events() RETURNS VOID AS $_$
194 /**
195   * Deleting expired events without simultaneously deleting their outputs
196   * creates orphaned outputs.  Deleting their outputs and all of the events 
197   * linking back to them, plus any outputs those events link to is messy and 
198   * inefficient.  It's simpler to handle them in 2 sweeping steps.
199   *
200   * 1. Delete expired events.
201   * 2. Delete orphaned event outputs.
202   *
203   * This has the added benefit of removing outputs that may have been
204   * orphaned by some other process.  Such outputs are not usuable by
205   * the system.
206   *
207   * This does not guarantee that all events within an event group are
208   * purged at the same time.  In such cases, the remaining events will
209   * be purged with the next instance of the purge (or soon thereafter).
210   * This is another nod toward efficiency over completeness of old 
211   * data that's circling the bit bucket anyway.
212   */
213 BEGIN
214
215     DELETE FROM action_trigger.event WHERE id IN (
216         SELECT evt.id
217         FROM action_trigger.event evt
218         JOIN action_trigger.event_definition def ON (def.id = evt.event_def)
219         WHERE def.retention_interval IS NOT NULL 
220             AND evt.state <> 'pending'
221             AND evt.update_time < (NOW() - def.retention_interval)
222     );
223
224     WITH linked_outputs AS (
225         SELECT templates.id AS id FROM (
226             SELECT DISTINCT(template_output) AS id
227                 FROM action_trigger.event WHERE template_output IS NOT NULL
228             UNION
229             SELECT DISTINCT(error_output) AS id
230                 FROM action_trigger.event WHERE error_output IS NOT NULL
231             UNION
232             SELECT DISTINCT(async_output) AS id
233                 FROM action_trigger.event WHERE async_output IS NOT NULL
234         ) templates
235     ) DELETE FROM action_trigger.event_output
236         WHERE id NOT IN (SELECT id FROM linked_outputs);
237
238 END;
239 $_$ LANGUAGE PLPGSQL;
240
241
242 /* -- UNDO --
243
244 DROP FUNCTION IF EXISTS action_trigger.purge_events();
245 DROP TRIGGER IF EXISTS is_valid_retention_interval ON action_trigger.event_definition;
246 DROP FUNCTION IF EXISTS action_trigger.check_valid_retention_interval();
247 ALTER TABLE action_trigger.event_definition DROP COLUMN retention_interval;
248
249 */
250
251
252
253 SELECT evergreen.upgrade_deps_block_check('1044', :eg_version);
254
255 UPDATE action_trigger.hook SET passive = FALSE WHERE key IN (
256     'format.po.html',
257     'format.po.pdf',
258     'format.selfcheck.checkout',
259     'format.selfcheck.items_out',
260     'format.selfcheck.holds',
261     'format.selfcheck.fines',
262     'format.acqcle.html',
263     'format.acqinv.html',
264     'format.acqli.html',
265     'aur.ordered',
266     'aur.received',
267     'aur.cancelled',
268     'aur.created',
269     'aur.rejected'
270 );
271
272
273 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1045', :eg_version); -- csharp/berick/gmcharlt
274
275 ALTER TABLE action.transit_copy
276         ADD COLUMN cancel_time TIMESTAMPTZ;
277
278 -- change "abort" to "cancel" in stock perm descriptions
279 UPDATE permission.perm_list 
280         SET description = 'Allow a user to cancel a copy transit if the user is at the transit destination or source' 
281         WHERE code = 'ABORT_TRANSIT'
282         AND description = 'Allow a user to abort a copy transit if the user is at the transit destination or source';
283 UPDATE permission.perm_list 
284         SET description = 'Allow a user to cancel a copy transit if the user is not at the transit source or dest' 
285         WHERE code = 'ABORT_REMOTE_TRANSIT'
286         AND description = 'Allow a user to abort a copy transit if the user is not at the transit source or dest';
287 UPDATE permission.perm_list 
288         SET description = 'Allows a user to cancel a transit on a copy with status of LOST' 
289         WHERE code = 'ABORT_TRANSIT_ON_LOST'
290         AND description = 'Allows a user to abort a transit on a copy with status of LOST';
291 UPDATE permission.perm_list 
292         SET description = 'Allows a user to cancel a transit on a copy with status of MISSING' 
293         WHERE code = 'ABORT_TRANSIT_ON_MISSING'
294         AND description = 'Allows a user to abort a transit on a copy with status of MISSING';
295
296 SELECT evergreen.upgrade_deps_block_check('1046', :eg_version); -- phasefx/berick/gmcharlt
297
298 INSERT into config.org_unit_setting_type (
299      name
300     ,grp
301     ,label
302     ,description
303     ,datatype
304 ) VALUES ( ----------------------------------------
305      'webstaff.format.dates'
306     ,'gui'
307     ,oils_i18n_gettext(
308          'webstaff.format.dates'
309         ,'Format Dates with this pattern'
310         ,'coust'
311         ,'label'
312     )
313     ,oils_i18n_gettext(
314          'webstaff.format.dates'
315         ,'Format Dates with this pattern (examples: "yyyy-MM-dd" for "2010-04-26", "MMM d, yyyy" for "Apr 26, 2010").  This will be used in areas where a date without a timestamp is sufficient, like Date of Birth.'
316         ,'coust'
317         ,'description'
318     )
319     ,'string'
320 ), ( ----------------------------------------
321      'webstaff.format.date_and_time'
322     ,'gui'
323     ,oils_i18n_gettext(
324          'webstaff.format.date_and_time'
325         ,'Format Date+Time with this pattern'
326         ,'coust'
327         ,'label'
328     )
329     ,oils_i18n_gettext(
330          'webstaff.format.date_and_time'
331         ,'Format Date+Time with this pattern (examples: "yy-MM-dd h:m:s.SSS a" for "16-04-05 2:07:20.666 PM", "yyyy-dd-MMM HH:mm" for "2016-05-Apr 14:07").  This will be used in areas of the client where a date with a timestamp is needed, like Checkout, Due Date, or Record Created.'
332         ,'coust'
333         ,'description'
334     )
335     ,'string'
336 );
337
338 UPDATE
339     config.org_unit_setting_type
340 SET
341     label = 'Deprecated: ' || label -- FIXME: Is this okay?
342 WHERE
343     name IN ('format.date','format.time')
344 ;
345
346
347 SELECT evergreen.upgrade_deps_block_check('1047', :eg_version); -- gmcharlt/stompro
348
349 CREATE TABLE config.copy_tag_type (
350     code            TEXT NOT NULL PRIMARY KEY,
351     label           TEXT NOT NULL,
352     owner           INTEGER NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED
353 );
354
355 CREATE INDEX config_copy_tag_type_owner_idx
356     ON config.copy_tag_type (owner);
357
358 CREATE TABLE asset.copy_tag (
359     id              SERIAL PRIMARY KEY,
360     tag_type        TEXT REFERENCES config.copy_tag_type (code)
361                     ON UPDATE CASCADE ON DELETE CASCADE,
362     label           TEXT NOT NULL,
363     value           TEXT NOT NULL,
364     index_vector    tsvector NOT NULL,
365     staff_note      TEXT,
366     pub             BOOLEAN DEFAULT TRUE,
367     owner           INTEGER NOT NULL REFERENCES actor.org_unit (id)
368 );
369
370 CREATE INDEX asset_copy_tag_label_idx
371     ON asset.copy_tag (label);
372 CREATE INDEX asset_copy_tag_label_lower_idx
373     ON asset.copy_tag (evergreen.lowercase(label));
374 CREATE INDEX asset_copy_tag_index_vector_idx
375     ON asset.copy_tag
376     USING GIN(index_vector);
377 CREATE INDEX asset_copy_tag_tag_type_idx
378     ON asset.copy_tag (tag_type);
379 CREATE INDEX asset_copy_tag_owner_idx
380     ON asset.copy_tag (owner);
381
382 CREATE OR REPLACE FUNCTION asset.set_copy_tag_value () RETURNS TRIGGER AS $$
383 BEGIN
384     IF NEW.value IS NULL THEN
385         NEW.value = NEW.label;        
386     END IF;
387
388     RETURN NEW;
389 END;
390 $$ LANGUAGE 'plpgsql';
391
392 -- name of following trigger chosen to ensure it runs first
393 CREATE TRIGGER asset_copy_tag_do_value
394     BEFORE INSERT OR UPDATE ON asset.copy_tag
395     FOR EACH ROW EXECUTE PROCEDURE asset.set_copy_tag_value();
396 CREATE TRIGGER asset_copy_tag_fti_trigger
397     BEFORE UPDATE OR INSERT ON asset.copy_tag
398     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('default');
399
400 CREATE TABLE asset.copy_tag_copy_map (
401     id              BIGSERIAL PRIMARY KEY,
402     copy            BIGINT REFERENCES asset.copy (id)
403                     ON UPDATE CASCADE ON DELETE CASCADE,
404     tag             INTEGER REFERENCES asset.copy_tag (id)
405                     ON UPDATE CASCADE ON DELETE CASCADE
406 );
407
408 CREATE INDEX asset_copy_tag_copy_map_copy_idx
409     ON asset.copy_tag_copy_map (copy);
410 CREATE INDEX asset_copy_tag_copy_map_tag_idx
411     ON asset.copy_tag_copy_map (tag);
412
413 INSERT INTO config.copy_tag_type (code, label, owner) VALUES ('bookplate', 'Digital Bookplate', 1);
414
415 INSERT INTO permission.perm_list ( id, code, description ) VALUES
416  ( 590, 'ADMIN_COPY_TAG_TYPES', oils_i18n_gettext( 590,
417     'Administer copy tag types', 'ppl', 'description' )),
418  ( 591, 'ADMIN_COPY_TAG', oils_i18n_gettext( 591,
419     'Administer copy tag', 'ppl', 'description' ))
420 ;
421
422 INSERT INTO config.org_unit_setting_type
423     (name, label, description, grp, datatype)
424 VALUES (
425     'opac.search.enable_bookplate_search',
426     oils_i18n_gettext(
427         'opac.search.enable_bookplate_search',
428         'Enable Digital Bookplate Search',
429         'coust',
430         'label'
431     ),
432     oils_i18n_gettext(
433         'opac.search.enable_bookplate_search',
434         'If enabled, adds a "Digital Bookplate" option to the query type selectors in the public catalog for search on copy tags.',   
435         'coust',
436         'description'
437     ),
438     'opac',
439     'bool'
440 );
441
442
443 SELECT evergreen.upgrade_deps_block_check('1048', :eg_version);
444
445 INSERT into config.org_unit_setting_type (
446      name
447     ,grp
448     ,label
449     ,description
450     ,datatype
451 ) VALUES ( ----------------------------------------
452      'webstaff.cat.label.font.family'
453     ,'cat'
454     ,oils_i18n_gettext(
455          'webstaff.cat.label.font.family'
456         ,'Item Print Label Font Family'
457         ,'coust'
458         ,'label'
459     )
460     ,oils_i18n_gettext(
461          'webstaff.cat.label.font.family'
462         ,'Set the preferred font family for item print labels. You can specify a list of CSS fonts, separated by commas, in order of preference; the system will use the first font it finds with a matching name. For example, "Arial, Helvetica, serif"'
463         ,'coust'
464         ,'description'
465     )
466     ,'string'
467 ), ( ----------------------------------------
468      'webstaff.cat.label.font.size'
469     ,'cat'
470     ,oils_i18n_gettext(
471          'webstaff.cat.label.font.size'
472         ,'Item Print Label Font Size'
473         ,'coust'
474         ,'label'
475     )
476     ,oils_i18n_gettext(
477          'webstaff.cat.label.font.size'
478         ,'Set the default font size for item print labels. Please include a unit of measurement that is valid CSS. For example, "12pt" or "16px" or "1em"'
479         ,'coust'
480         ,'description'
481     )
482     ,'string'
483 ), ( ----------------------------------------
484      'webstaff.cat.label.font.weight'
485     ,'cat'
486     ,oils_i18n_gettext(
487          'webstaff.cat.label.font.weight'
488         ,'Item Print Label Font Weight'
489         ,'coust'
490         ,'label'
491     )
492     ,oils_i18n_gettext(
493          'webstaff.cat.label.font.weight'
494         ,'Set the default font weight for item print labels. Please use the CSS specification for values for font-weight.  For example, "normal", "bold", "bolder", or "lighter"'
495         ,'coust'
496         ,'description'
497     )
498     ,'string'
499 ), ( ----------------------------------------
500      'webstaff.cat.label.left_label.left_margin'
501     ,'cat'
502     ,oils_i18n_gettext(
503          'webstaff.cat.label.left_label.left_margin'
504         ,'Item Print Label - Left Margin for Left Label'
505         ,'coust'
506         ,'label'
507     )
508     ,oils_i18n_gettext(
509          'webstaff.cat.label.left_label.left_margin'
510         ,'Set the default left margin for the leftmost item print Label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
511         ,'coust'
512         ,'description'
513     )
514     ,'string'
515 ), ( ----------------------------------------
516      'webstaff.cat.label.right_label.left_margin'
517     ,'cat'
518     ,oils_i18n_gettext(
519          'webstaff.cat.label.right_label.left_margin'
520         ,'Item Print Label - Left Margin for Right Label'
521         ,'coust'
522         ,'label'
523     )
524     ,oils_i18n_gettext(
525          'webstaff.cat.label.right_label.left_margin'
526         ,'Set the default left margin for the rightmost item print label (or in other words, the desired space between the two labels). Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
527         ,'coust'
528         ,'description'
529     )
530     ,'string'
531 ), ( ----------------------------------------
532      'webstaff.cat.label.left_label.height'
533     ,'cat'
534     ,oils_i18n_gettext(
535          'webstaff.cat.label.left_label.height'
536         ,'Item Print Label - Height for Left Label'
537         ,'coust'
538         ,'label'
539     )
540     ,oils_i18n_gettext(
541          'webstaff.cat.label.left_label.height'
542         ,'Set the default height for the leftmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
543         ,'coust'
544         ,'description'
545     )
546     ,'string'
547 ), ( ----------------------------------------
548      'webstaff.cat.label.left_label.width'
549     ,'cat'
550     ,oils_i18n_gettext(
551          'webstaff.cat.label.left_label.width'
552         ,'Item Print Label - Width for Left Label'
553         ,'coust'
554         ,'label'
555     )
556     ,oils_i18n_gettext(
557          'webstaff.cat.label.left_label.width'
558         ,'Set the default width for the leftmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
559         ,'coust'
560         ,'description'
561     )
562     ,'string'
563 ), ( ----------------------------------------
564      'webstaff.cat.label.right_label.height'
565     ,'cat'
566     ,oils_i18n_gettext(
567          'webstaff.cat.label.right_label.height'
568         ,'Item Print Label - Height for Right Label'
569         ,'coust'
570         ,'label'
571     )
572     ,oils_i18n_gettext(
573          'webstaff.cat.label.right_label.height'
574         ,'Set the default height for the rightmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
575         ,'coust'
576         ,'description'
577     )
578     ,'string'
579 ), ( ----------------------------------------
580      'webstaff.cat.label.right_label.width'
581     ,'cat'
582     ,oils_i18n_gettext(
583          'webstaff.cat.label.right_label.width'
584         ,'Item Print Label - Width for Right Label'
585         ,'coust'
586         ,'label'
587     )
588     ,oils_i18n_gettext(
589          'webstaff.cat.label.right_label.width'
590         ,'Set the default width for the rightmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
591         ,'coust'
592         ,'description'
593     )
594     ,'string'
595 ), (
596      'webstaff.cat.label.inline_css'
597     ,'cat'
598     ,oils_i18n_gettext(
599          'webstaff.cat.label.inline_css'
600         ,'Item Print Label - Inline CSS'
601         ,'coust'
602         ,'label'
603     )
604     ,oils_i18n_gettext(
605          'webstaff.cat.label.inline_css'
606         ,'This setting allows you to inject arbitrary CSS into the item print label template.  For example, ".printlabel { text-transform: uppercase; }"'
607         ,'coust'
608         ,'description'
609     )
610     ,'string'
611 ), (
612      'webstaff.cat.label.call_number_wrap_filter_height'
613     ,'cat'
614     ,oils_i18n_gettext(
615          'webstaff.cat.label.call_number_wrap_filter_height'
616         ,'Item Print Label - Call Number Wrap Filter Height'
617         ,'coust'
618         ,'label'
619     )
620     ,oils_i18n_gettext(
621          'webstaff.cat.label.call_number_wrap_filter_height'
622         ,'This setting is used to set the default height (in number of lines) to use for call number wrapping in the left print label.'
623         ,'coust'
624         ,'description'
625     )
626     ,'integer'
627 ), (
628      'webstaff.cat.label.call_number_wrap_filter_width'
629     ,'cat'
630     ,oils_i18n_gettext(
631          'webstaff.cat.label.call_number_wrap_filter_width'
632         ,'Item Print Label - Call Number Wrap Filter Width'
633         ,'coust'
634         ,'label'
635     )
636     ,oils_i18n_gettext(
637          'webstaff.cat.label.call_number_wrap_filter_width'
638         ,'This setting is used to set the default width (in number of characters) to use for call number wrapping in the left print label.'
639         ,'coust'
640         ,'description'
641     )
642     ,'integer'
643
644
645 );
646
647 -- for testing, setting removal:
648 --DELETE FROM actor.org_unit_setting WHERE name IN (
649 --     'webstaff.cat.label.font.family'
650 --    ,'webstaff.cat.label.font.size'
651 --    ,'webstaff.cat.label.font.weight'
652 --    ,'webstaff.cat.label.left_label.height'
653 --    ,'webstaff.cat.label.left_label.width'
654 --    ,'webstaff.cat.label.left_label.left_margin'
655 --    ,'webstaff.cat.label.right_label.height'
656 --    ,'webstaff.cat.label.right_label.width'
657 --    ,'webstaff.cat.label.right_label.left_margin'
658 --    ,'webstaff.cat.label.inline_css'
659 --    ,'webstaff.cat.label.call_number_wrap_filter_height'
660 --    ,'webstaff.cat.label.call_number_wrap_filter_width'
661 --);
662 --DELETE FROM config.org_unit_setting_type_log WHERE field_name IN (
663 --     'webstaff.cat.label.font.family'
664 --    ,'webstaff.cat.label.font.size'
665 --    ,'webstaff.cat.label.font.weight'
666 --    ,'webstaff.cat.label.left_label.height'
667 --    ,'webstaff.cat.label.left_label.width'
668 --    ,'webstaff.cat.label.left_label.left_margin'
669 --    ,'webstaff.cat.label.right_label.height'
670 --    ,'webstaff.cat.label.right_label.width'
671 --    ,'webstaff.cat.label.right_label.left_margin'
672 --    ,'webstaff.cat.label.inline_css'
673 --    ,'webstaff.cat.label.call_number_wrap_filter_height'
674 --    ,'webstaff.cat.label.call_number_wrap_filter_width'
675 --);
676 --DELETE FROM config.org_unit_setting_type WHERE name IN (
677 --     'webstaff.cat.label.font.family'
678 --    ,'webstaff.cat.label.font.size'
679 --    ,'webstaff.cat.label.font.weight'
680 --    ,'webstaff.cat.label.left_label.height'
681 --    ,'webstaff.cat.label.left_label.width'
682 --    ,'webstaff.cat.label.left_label.left_margin'
683 --    ,'webstaff.cat.label.right_label.height'
684 --    ,'webstaff.cat.label.right_label.width'
685 --    ,'webstaff.cat.label.right_label.left_margin'
686 --    ,'webstaff.cat.label.inline_css'
687 --    ,'webstaff.cat.label.call_number_wrap_filter_height'
688 --    ,'webstaff.cat.label.call_number_wrap_filter_width'
689 --);
690
691
692
693 SELECT evergreen.upgrade_deps_block_check('1049', :eg_version); -- mmorgan/stompro/gmcharlt
694
695 \echo -----------------------------------------------------------
696 \echo Setting invalid age_protect and circ_as_type entries to NULL,
697 \echo otherwise they will break the Serial Copy Templates editor.
698 \echo Please review any Serial Copy Templates listed below.
699 \echo
700 UPDATE asset.copy_template act
701 SET age_protect = NULL
702 FROM actor.org_unit aou
703 WHERE aou.id=act.owning_lib
704    AND act.age_protect NOT IN
705    (
706    SELECT id FROM config.rule_age_hold_protect
707    )
708 RETURNING act.id "Template ID", act.name "Template Name",
709           aou.shortname "Owning Lib",
710           'Age Protection value reset to null.' "Description";
711
712 UPDATE asset.copy_template act
713 SET circ_as_type = NULL
714 FROM actor.org_unit aou
715 WHERE aou.id=act.owning_lib
716    AND act.circ_as_type NOT IN
717    (
718    SELECT code FROM config.item_type_map
719    )
720 RETURNING act.id "Template ID", act.name "Template Name",
721           aou.shortname "Owning Lib",
722           'Circ as Type value reset to null.' as "Description";
723
724 \echo -----------End Serial Template Fix----------------
725
726 SELECT evergreen.upgrade_deps_block_check('1050', :eg_version); -- mmorgan/cesardv/gmcharlt
727
728 CREATE OR REPLACE FUNCTION permission.usr_perms ( INT ) RETURNS SETOF permission.usr_perm_map AS $$
729     SELECT      DISTINCT ON (usr,perm) *
730           FROM  (
731                         (SELECT * FROM permission.usr_perm_map WHERE usr = $1)
732             UNION ALL
733                         (SELECT -p.id, $1 AS usr, p.perm, p.depth, p.grantable
734                           FROM  permission.grp_perm_map p
735                           WHERE p.grp IN (
736       SELECT    (permission.grp_ancestors(
737       (SELECT profile FROM actor.usr WHERE id = $1)
738                                         )).id
739                                 )
740                         )
741             UNION ALL
742                         (SELECT -p.id, $1 AS usr, p.perm, p.depth, p.grantable
743                           FROM  permission.grp_perm_map p
744                           WHERE p.grp IN (SELECT (permission.grp_ancestors(m.grp)).id FROM permission.usr_grp_map m WHERE usr = $1))
745                 ) AS x
746           ORDER BY 2, 3, 4 ASC, 5 DESC ;
747 $$ LANGUAGE SQL STABLE ROWS 10;
748
749 SELECT evergreen.upgrade_deps_block_check('1051', :eg_version);
750
751 CREATE OR REPLACE VIEW action.all_circulation_slim AS
752     SELECT
753         id,
754         usr,
755         xact_start,
756         xact_finish,
757         unrecovered,
758         target_copy,
759         circ_lib,
760         circ_staff,
761         checkin_staff,
762         checkin_lib,
763         renewal_remaining,
764         grace_period,
765         due_date,
766         stop_fines_time,
767         checkin_time,
768         create_time,
769         duration,
770         fine_interval,
771         recurring_fine,
772         max_fine,
773         phone_renewal,
774         desk_renewal,
775         opac_renewal,
776         duration_rule,
777         recurring_fine_rule,
778         max_fine_rule,
779         stop_fines,
780         workstation,
781         checkin_workstation,
782         copy_location,
783         checkin_scan_time,
784         parent_circ
785     FROM action.circulation
786 UNION ALL
787     SELECT
788         id,
789         NULL AS usr,
790         xact_start,
791         xact_finish,
792         unrecovered,
793         target_copy,
794         circ_lib,
795         circ_staff,
796         checkin_staff,
797         checkin_lib,
798         renewal_remaining,
799         grace_period,
800         due_date,
801         stop_fines_time,
802         checkin_time,
803         create_time,
804         duration,
805         fine_interval,
806         recurring_fine,
807         max_fine,
808         phone_renewal,
809         desk_renewal,
810         opac_renewal,
811         duration_rule,
812         recurring_fine_rule,
813         max_fine_rule,
814         stop_fines,
815         workstation,
816         checkin_workstation,
817         copy_location,
818         checkin_scan_time,
819         parent_circ
820     FROM action.aged_circulation
821 ;
822
823 DROP FUNCTION action.summarize_all_circ_chain(INTEGER);
824 DROP FUNCTION action.all_circ_chain(INTEGER);
825
826 CREATE OR REPLACE FUNCTION action.all_circ_chain (ctx_circ_id INTEGER) 
827     RETURNS SETOF action.all_circulation_slim AS $$
828 DECLARE
829     tmp_circ action.all_circulation_slim%ROWTYPE;
830     circ_0 action.all_circulation_slim%ROWTYPE;
831 BEGIN
832
833     SELECT INTO tmp_circ * FROM action.all_circulation_slim WHERE id = ctx_circ_id;
834
835     IF tmp_circ IS NULL THEN
836         RETURN NEXT tmp_circ;
837     END IF;
838     circ_0 := tmp_circ;
839
840     -- find the front of the chain
841     WHILE TRUE LOOP
842         SELECT INTO tmp_circ * FROM action.all_circulation_slim 
843             WHERE id = tmp_circ.parent_circ;
844         IF tmp_circ IS NULL THEN
845             EXIT;
846         END IF;
847         circ_0 := tmp_circ;
848     END LOOP;
849
850     -- now send the circs to the caller, oldest to newest
851     tmp_circ := circ_0;
852     WHILE TRUE LOOP
853         IF tmp_circ IS NULL THEN
854             EXIT;
855         END IF;
856         RETURN NEXT tmp_circ;
857         SELECT INTO tmp_circ * FROM action.all_circulation_slim 
858             WHERE parent_circ = tmp_circ.id;
859     END LOOP;
860
861 END;
862 $$ LANGUAGE 'plpgsql';
863
864 CREATE OR REPLACE FUNCTION action.summarize_all_circ_chain 
865     (ctx_circ_id INTEGER) RETURNS action.circ_chain_summary AS $$
866
867 DECLARE
868
869     -- first circ in the chain
870     circ_0 action.all_circulation_slim%ROWTYPE;
871
872     -- last circ in the chain
873     circ_n action.all_circulation_slim%ROWTYPE;
874
875     -- circ chain under construction
876     chain action.circ_chain_summary;
877     tmp_circ action.all_circulation_slim%ROWTYPE;
878
879 BEGIN
880     
881     chain.num_circs := 0;
882     FOR tmp_circ IN SELECT * FROM action.all_circ_chain(ctx_circ_id) LOOP
883
884         IF chain.num_circs = 0 THEN
885             circ_0 := tmp_circ;
886         END IF;
887
888         chain.num_circs := chain.num_circs + 1;
889         circ_n := tmp_circ;
890     END LOOP;
891
892     chain.start_time := circ_0.xact_start;
893     chain.last_stop_fines := circ_n.stop_fines;
894     chain.last_stop_fines_time := circ_n.stop_fines_time;
895     chain.last_checkin_time := circ_n.checkin_time;
896     chain.last_checkin_scan_time := circ_n.checkin_scan_time;
897     SELECT INTO chain.checkout_workstation name FROM actor.workstation WHERE id = circ_0.workstation;
898     SELECT INTO chain.last_checkin_workstation name FROM actor.workstation WHERE id = circ_n.checkin_workstation;
899
900     IF chain.num_circs > 1 THEN
901         chain.last_renewal_time := circ_n.xact_start;
902         SELECT INTO chain.last_renewal_workstation name FROM actor.workstation WHERE id = circ_n.workstation;
903     END IF;
904
905     RETURN chain;
906
907 END;
908 $$ LANGUAGE 'plpgsql';
909
910 CREATE OR REPLACE FUNCTION rating.percent_time_circulating(badge_id INT)
911     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
912 DECLARE
913     badge   rating.badge_with_orgs%ROWTYPE;
914 BEGIN
915
916     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
917
918     PERFORM rating.precalc_bibs_by_copy(badge_id);
919
920     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
921         SELECT id FROM precalc_filter_bib_list
922             INTERSECT
923         SELECT id FROM precalc_bibs_by_copy_list
924     );
925
926     ANALYZE precalc_copy_filter_bib_list;
927
928     RETURN QUERY
929      SELECT bib,
930             SUM(COALESCE(circ_time,0))::NUMERIC / SUM(age)::NUMERIC
931       FROM  (SELECT cn.record AS bib,
932                     cp.id,
933                     EXTRACT( EPOCH FROM AGE(cp.active_date) ) + 1 AS age,
934                     SUM(  -- time copy spent circulating
935                         EXTRACT(
936                             EPOCH FROM
937                             AGE(
938                                 COALESCE(circ.checkin_time, circ.stop_fines_time, NOW()),
939                                 circ.xact_start
940                             )
941                         )
942                     )::NUMERIC AS circ_time
943               FROM  asset.copy cp
944                     JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
945                     JOIN asset.call_number cn ON (cn.id = cp.call_number)
946                     LEFT JOIN action.all_circulation_slim circ ON (
947                         circ.target_copy = cp.id
948                         AND stop_fines NOT IN (
949                             'LOST',
950                             'LONGOVERDUE',
951                             'CLAIMSRETURNED',
952                             'LONGOVERDUE'
953                         )
954                         AND NOT (
955                             checkin_time IS NULL AND
956                             stop_fines = 'MAXFINES'
957                         )
958                     )
959               WHERE cn.owning_lib = ANY (badge.orgs)
960                     AND cp.active_date IS NOT NULL
961                     -- Next line requires that copies with no circs (circ.id IS NULL) also not be deleted
962                     AND ((circ.id IS NULL AND NOT cp.deleted) OR circ.id IS NOT NULL)
963               GROUP BY 1,2,3
964             ) x
965       GROUP BY 1;
966 END;
967 $f$ LANGUAGE PLPGSQL STRICT;
968
969
970 -- ROLLBACK;
971
972
973 SELECT evergreen.upgrade_deps_block_check('1052', :eg_version);
974
975 CREATE OR REPLACE FUNCTION rating.inhouse_over_time(badge_id INT)
976     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
977 DECLARE
978     badge   rating.badge_with_orgs%ROWTYPE;
979     iage    INT     := 1;
980     iint    INT     := NULL;
981     iscale  NUMERIC := NULL;
982 BEGIN
983
984     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
985
986     IF badge.horizon_age IS NULL THEN
987         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
988             badge.name,
989             badge.id;
990     END IF;
991
992     PERFORM rating.precalc_bibs_by_copy(badge_id);
993
994     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
995         SELECT id FROM precalc_filter_bib_list
996             INTERSECT
997         SELECT id FROM precalc_bibs_by_copy_list
998     );
999
1000     ANALYZE precalc_copy_filter_bib_list;
1001
1002     iint := EXTRACT(EPOCH FROM badge.importance_interval);
1003     IF badge.importance_age IS NOT NULL THEN
1004         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
1005     END IF;
1006
1007     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
1008     iscale := COALESCE(badge.importance_scale, 1.0);
1009
1010     RETURN QUERY
1011      SELECT bib,
1012             SUM( uses * GREATEST( iscale * (iage - cage), 1.0 ))
1013       FROM (
1014          SELECT cn.record AS bib,
1015                 (1 + EXTRACT(EPOCH FROM AGE(u.use_time)) / iint)::INT AS cage,
1016                 COUNT(u.id)::INT AS uses
1017           FROM  action.in_house_use u
1018                 JOIN precalc_copy_filter_bib_list cf ON (u.item = cf.copy)
1019                 JOIN asset.copy cp ON (cp.id = u.item)
1020                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
1021           WHERE u.use_time >= NOW() - badge.horizon_age
1022                 AND cn.owning_lib = ANY (badge.orgs)
1023           GROUP BY 1, 2
1024       ) x
1025       GROUP BY 1;
1026 END;
1027 $f$ LANGUAGE PLPGSQL STRICT;
1028
1029 INSERT INTO rating.popularity_parameter (id, name, func, require_horizon,require_percentile) VALUES
1030     (18,'In-House Use Over Time', 'rating.inhouse_over_time', TRUE, TRUE);
1031
1032
1033
1034 SELECT evergreen.upgrade_deps_block_check('1053', :eg_version);
1035
1036 CREATE OR REPLACE FUNCTION rating.org_unit_count(badge_id INT)
1037     RETURNS TABLE (record INT, value NUMERIC) AS $f$
1038 DECLARE
1039     badge   rating.badge_with_orgs%ROWTYPE;
1040 BEGIN
1041
1042     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
1043
1044     PERFORM rating.precalc_bibs_by_copy(badge_id);
1045
1046     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
1047         SELECT id FROM precalc_filter_bib_list
1048             INTERSECT
1049         SELECT id FROM precalc_bibs_by_copy_list
1050     );
1051     ANALYZE precalc_copy_filter_bib_list;
1052
1053     -- Use circ rather than owning lib here as that means "on the shelf at..."
1054     RETURN QUERY
1055      SELECT f.id::INT AS bib,
1056             COUNT(DISTINCT cp.circ_lib)::NUMERIC
1057      FROM asset.copy cp
1058           JOIN precalc_copy_filter_bib_list f ON (cp.id = f.copy)
1059      WHERE cp.circ_lib = ANY (badge.orgs) GROUP BY 1;
1060
1061 END;
1062 $f$ LANGUAGE PLPGSQL STRICT;
1063
1064 INSERT INTO rating.popularity_parameter (id, name, func, require_percentile) VALUES
1065     (17,'Circulation Library Count', 'rating.org_unit_count', TRUE);
1066
1067
1068
1069 SELECT evergreen.upgrade_deps_block_check('1054', :eg_version);
1070
1071 INSERT into config.org_unit_setting_type
1072 ( name, grp, label, description, datatype ) VALUES
1073
1074 ( 'lib.timezone', 'lib',
1075     oils_i18n_gettext('lib.timezone',
1076         'Library time zone',
1077         'coust', 'label'),
1078     oils_i18n_gettext('lib.timezone',
1079         'Define the time zone in which a library physically resides',
1080         'coust', 'description'),
1081     'string');
1082
1083 ALTER TABLE actor.org_unit_closed ADD COLUMN full_day BOOLEAN DEFAULT FALSE;
1084 ALTER TABLE actor.org_unit_closed ADD COLUMN multi_day BOOLEAN DEFAULT FALSE;
1085
1086 UPDATE actor.org_unit_closed SET multi_day = TRUE
1087   WHERE close_start::DATE <> close_end::DATE;
1088
1089 UPDATE actor.org_unit_closed SET full_day = TRUE
1090   WHERE close_start::DATE = close_end::DATE
1091         AND SUBSTRING(close_start::time::text FROM 1 FOR 8) = '00:00:00'
1092         AND SUBSTRING(close_end::time::text FROM 1 FOR 8) = '23:59:59';
1093
1094 CREATE OR REPLACE FUNCTION action.push_circ_due_time () RETURNS TRIGGER AS $$
1095 DECLARE
1096     proper_tz TEXT := COALESCE(
1097         oils_json_to_text((
1098             SELECT value
1099               FROM  actor.org_unit_ancestor_setting('lib.timezone',NEW.circ_lib)
1100               LIMIT 1
1101         )),
1102         CURRENT_SETTING('timezone')
1103     );
1104 BEGIN
1105
1106     IF (EXTRACT(EPOCH FROM NEW.duration)::INT % EXTRACT(EPOCH FROM '1 day'::INTERVAL)::INT) = 0 -- day-granular duration
1107         AND SUBSTRING((NEW.due_date AT TIME ZONE proper_tz)::TIME::TEXT FROM 1 FOR 8) <> '23:59:59' THEN -- has not yet been pushed
1108         NEW.due_date = ((NEW.due_date AT TIME ZONE proper_tz)::DATE + '1 day'::INTERVAL - '1 second'::INTERVAL) || ' ' || proper_tz;
1109     END IF;
1110
1111     RETURN NEW;
1112 END;
1113 $$ LANGUAGE PLPGSQL;
1114
1115
1116 \qecho The following query will adjust all historical, unaged circulations so
1117 \qecho that if their due date field is pushed to the end of the day, it is done
1118 \qecho in the circulating library''''s time zone, and not the server time zone.
1119 \qecho 
1120 \qecho It is safe to run this after any change to library time zones.
1121 \qecho 
1122 \qecho Running this is not required, as no code before this change has
1123 \qecho depended on the time string of '''23:59:59'''.  It is also not necessary
1124 \qecho if all of your libraries are in the same time zone, and that time zone
1125 \qecho is the same as the database''''s configured time zone.
1126 \qecho 
1127 \qecho 'DO $$'
1128 \qecho 'declare'
1129 \qecho '    new_tz  text;'
1130 \qecho '    ou_id   int;'
1131 \qecho 'begin'
1132 \qecho '    for ou_id in select id from actor.org_unit loop'
1133 \qecho '        for new_tz in select oils_json_to_text(value) from actor.org_unit_ancestor_setting('''lib.timezone''',ou_id) loop'
1134 \qecho '            if new_tz is not null then'
1135 \qecho '                update  action.circulation'
1136 \qecho '                  set   due_date = (due_date::timestamp || ''' ''' || new_tz)::timestamptz'
1137 \qecho '                  where circ_lib = ou_id'
1138 \qecho '                        and substring((due_date at time zone new_tz)::time::text from 1 for 8) <> '''23:59:59''';'
1139 \qecho '            end if;'
1140 \qecho '        end loop;'
1141 \qecho '    end loop;'
1142 \qecho 'end;'
1143 \qecho '$$;'
1144 \qecho 
1145
1146 SELECT evergreen.upgrade_deps_block_check('1055', :eg_version);
1147
1148 CREATE OR REPLACE FUNCTION actor.usr_merge( src_usr INT, dest_usr INT, del_addrs BOOLEAN, del_cards BOOLEAN, deactivate_cards BOOLEAN ) RETURNS VOID AS $$
1149 DECLARE
1150         suffix TEXT;
1151         bucket_row RECORD;
1152         picklist_row RECORD;
1153         queue_row RECORD;
1154         folder_row RECORD;
1155 BEGIN
1156
1157     -- do some initial cleanup 
1158     UPDATE actor.usr SET card = NULL WHERE id = src_usr;
1159     UPDATE actor.usr SET mailing_address = NULL WHERE id = src_usr;
1160     UPDATE actor.usr SET billing_address = NULL WHERE id = src_usr;
1161
1162     -- actor.*
1163     IF del_cards THEN
1164         DELETE FROM actor.card where usr = src_usr;
1165     ELSE
1166         IF deactivate_cards THEN
1167             UPDATE actor.card SET active = 'f' WHERE usr = src_usr;
1168         END IF;
1169         UPDATE actor.card SET usr = dest_usr WHERE usr = src_usr;
1170     END IF;
1171
1172
1173     IF del_addrs THEN
1174         DELETE FROM actor.usr_address WHERE usr = src_usr;
1175     ELSE
1176         UPDATE actor.usr_address SET usr = dest_usr WHERE usr = src_usr;
1177     END IF;
1178
1179     UPDATE actor.usr_note SET usr = dest_usr WHERE usr = src_usr;
1180     -- dupes are technically OK in actor.usr_standing_penalty, should manually delete them...
1181     UPDATE actor.usr_standing_penalty SET usr = dest_usr WHERE usr = src_usr;
1182     PERFORM actor.usr_merge_rows('actor.usr_org_unit_opt_in', 'usr', src_usr, dest_usr);
1183     PERFORM actor.usr_merge_rows('actor.usr_setting', 'usr', src_usr, dest_usr);
1184
1185     -- permission.*
1186     PERFORM actor.usr_merge_rows('permission.usr_perm_map', 'usr', src_usr, dest_usr);
1187     PERFORM actor.usr_merge_rows('permission.usr_object_perm_map', 'usr', src_usr, dest_usr);
1188     PERFORM actor.usr_merge_rows('permission.usr_grp_map', 'usr', src_usr, dest_usr);
1189     PERFORM actor.usr_merge_rows('permission.usr_work_ou_map', 'usr', src_usr, dest_usr);
1190
1191
1192     -- container.*
1193         
1194         -- For each *_bucket table: transfer every bucket belonging to src_usr
1195         -- into the custody of dest_usr.
1196         --
1197         -- In order to avoid colliding with an existing bucket owned by
1198         -- the destination user, append the source user's id (in parenthesese)
1199         -- to the name.  If you still get a collision, add successive
1200         -- spaces to the name and keep trying until you succeed.
1201         --
1202         FOR bucket_row in
1203                 SELECT id, name
1204                 FROM   container.biblio_record_entry_bucket
1205                 WHERE  owner = src_usr
1206         LOOP
1207                 suffix := ' (' || src_usr || ')';
1208                 LOOP
1209                         BEGIN
1210                                 UPDATE  container.biblio_record_entry_bucket
1211                                 SET     owner = dest_usr, name = name || suffix
1212                                 WHERE   id = bucket_row.id;
1213                         EXCEPTION WHEN unique_violation THEN
1214                                 suffix := suffix || ' ';
1215                                 CONTINUE;
1216                         END;
1217                         EXIT;
1218                 END LOOP;
1219         END LOOP;
1220
1221         FOR bucket_row in
1222                 SELECT id, name
1223                 FROM   container.call_number_bucket
1224                 WHERE  owner = src_usr
1225         LOOP
1226                 suffix := ' (' || src_usr || ')';
1227                 LOOP
1228                         BEGIN
1229                                 UPDATE  container.call_number_bucket
1230                                 SET     owner = dest_usr, name = name || suffix
1231                                 WHERE   id = bucket_row.id;
1232                         EXCEPTION WHEN unique_violation THEN
1233                                 suffix := suffix || ' ';
1234                                 CONTINUE;
1235                         END;
1236                         EXIT;
1237                 END LOOP;
1238         END LOOP;
1239
1240         FOR bucket_row in
1241                 SELECT id, name
1242                 FROM   container.copy_bucket
1243                 WHERE  owner = src_usr
1244         LOOP
1245                 suffix := ' (' || src_usr || ')';
1246                 LOOP
1247                         BEGIN
1248                                 UPDATE  container.copy_bucket
1249                                 SET     owner = dest_usr, name = name || suffix
1250                                 WHERE   id = bucket_row.id;
1251                         EXCEPTION WHEN unique_violation THEN
1252                                 suffix := suffix || ' ';
1253                                 CONTINUE;
1254                         END;
1255                         EXIT;
1256                 END LOOP;
1257         END LOOP;
1258
1259         FOR bucket_row in
1260                 SELECT id, name
1261                 FROM   container.user_bucket
1262                 WHERE  owner = src_usr
1263         LOOP
1264                 suffix := ' (' || src_usr || ')';
1265                 LOOP
1266                         BEGIN
1267                                 UPDATE  container.user_bucket
1268                                 SET     owner = dest_usr, name = name || suffix
1269                                 WHERE   id = bucket_row.id;
1270                         EXCEPTION WHEN unique_violation THEN
1271                                 suffix := suffix || ' ';
1272                                 CONTINUE;
1273                         END;
1274                         EXIT;
1275                 END LOOP;
1276         END LOOP;
1277
1278         UPDATE container.user_bucket_item SET target_user = dest_usr WHERE target_user = src_usr;
1279
1280     -- vandelay.*
1281         -- transfer queues the same way we transfer buckets (see above)
1282         FOR queue_row in
1283                 SELECT id, name
1284                 FROM   vandelay.queue
1285                 WHERE  owner = src_usr
1286         LOOP
1287                 suffix := ' (' || src_usr || ')';
1288                 LOOP
1289                         BEGIN
1290                                 UPDATE  vandelay.queue
1291                                 SET     owner = dest_usr, name = name || suffix
1292                                 WHERE   id = queue_row.id;
1293                         EXCEPTION WHEN unique_violation THEN
1294                                 suffix := suffix || ' ';
1295                                 CONTINUE;
1296                         END;
1297                         EXIT;
1298                 END LOOP;
1299         END LOOP;
1300
1301     -- money.*
1302     PERFORM actor.usr_merge_rows('money.collections_tracker', 'usr', src_usr, dest_usr);
1303     PERFORM actor.usr_merge_rows('money.collections_tracker', 'collector', src_usr, dest_usr);
1304     UPDATE money.billable_xact SET usr = dest_usr WHERE usr = src_usr;
1305     UPDATE money.billing SET voider = dest_usr WHERE voider = src_usr;
1306     UPDATE money.bnm_payment SET accepting_usr = dest_usr WHERE accepting_usr = src_usr;
1307
1308     -- action.*
1309     UPDATE action.circulation SET usr = dest_usr WHERE usr = src_usr;
1310     UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
1311     UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
1312     UPDATE action.usr_circ_history SET usr = dest_usr WHERE usr = src_usr;
1313
1314     UPDATE action.hold_request SET usr = dest_usr WHERE usr = src_usr;
1315     UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
1316     UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
1317     UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
1318
1319     UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
1320     UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
1321     UPDATE action.non_cataloged_circulation SET patron = dest_usr WHERE patron = src_usr;
1322     UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
1323     UPDATE action.survey_response SET usr = dest_usr WHERE usr = src_usr;
1324
1325     -- acq.*
1326     UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
1327         UPDATE acq.fund_transfer SET transfer_user = dest_usr WHERE transfer_user = src_usr;
1328
1329         -- transfer picklists the same way we transfer buckets (see above)
1330         FOR picklist_row in
1331                 SELECT id, name
1332                 FROM   acq.picklist
1333                 WHERE  owner = src_usr
1334         LOOP
1335                 suffix := ' (' || src_usr || ')';
1336                 LOOP
1337                         BEGIN
1338                                 UPDATE  acq.picklist
1339                                 SET     owner = dest_usr, name = name || suffix
1340                                 WHERE   id = picklist_row.id;
1341                         EXCEPTION WHEN unique_violation THEN
1342                                 suffix := suffix || ' ';
1343                                 CONTINUE;
1344                         END;
1345                         EXIT;
1346                 END LOOP;
1347         END LOOP;
1348
1349     UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
1350     UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
1351     UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
1352     UPDATE acq.provider_note SET creator = dest_usr WHERE creator = src_usr;
1353     UPDATE acq.provider_note SET editor = dest_usr WHERE editor = src_usr;
1354     UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
1355     UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
1356     UPDATE acq.lineitem_usr_attr_definition SET usr = dest_usr WHERE usr = src_usr;
1357
1358     -- asset.*
1359     UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
1360     UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
1361     UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
1362     UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
1363     UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
1364     UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
1365
1366     -- serial.*
1367     UPDATE serial.record_entry SET creator = dest_usr WHERE creator = src_usr;
1368     UPDATE serial.record_entry SET editor = dest_usr WHERE editor = src_usr;
1369
1370     -- reporter.*
1371     -- It's not uncommon to define the reporter schema in a replica 
1372     -- DB only, so don't assume these tables exist in the write DB.
1373     BEGIN
1374         UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
1375     EXCEPTION WHEN undefined_table THEN
1376         -- do nothing
1377     END;
1378     BEGIN
1379         UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
1380     EXCEPTION WHEN undefined_table THEN
1381         -- do nothing
1382     END;
1383     BEGIN
1384         UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
1385     EXCEPTION WHEN undefined_table THEN
1386         -- do nothing
1387     END;
1388     BEGIN
1389                 -- transfer folders the same way we transfer buckets (see above)
1390                 FOR folder_row in
1391                         SELECT id, name
1392                         FROM   reporter.template_folder
1393                         WHERE  owner = src_usr
1394                 LOOP
1395                         suffix := ' (' || src_usr || ')';
1396                         LOOP
1397                                 BEGIN
1398                                         UPDATE  reporter.template_folder
1399                                         SET     owner = dest_usr, name = name || suffix
1400                                         WHERE   id = folder_row.id;
1401                                 EXCEPTION WHEN unique_violation THEN
1402                                         suffix := suffix || ' ';
1403                                         CONTINUE;
1404                                 END;
1405                                 EXIT;
1406                         END LOOP;
1407                 END LOOP;
1408     EXCEPTION WHEN undefined_table THEN
1409         -- do nothing
1410     END;
1411     BEGIN
1412                 -- transfer folders the same way we transfer buckets (see above)
1413                 FOR folder_row in
1414                         SELECT id, name
1415                         FROM   reporter.report_folder
1416                         WHERE  owner = src_usr
1417                 LOOP
1418                         suffix := ' (' || src_usr || ')';
1419                         LOOP
1420                                 BEGIN
1421                                         UPDATE  reporter.report_folder
1422                                         SET     owner = dest_usr, name = name || suffix
1423                                         WHERE   id = folder_row.id;
1424                                 EXCEPTION WHEN unique_violation THEN
1425                                         suffix := suffix || ' ';
1426                                         CONTINUE;
1427                                 END;
1428                                 EXIT;
1429                         END LOOP;
1430                 END LOOP;
1431     EXCEPTION WHEN undefined_table THEN
1432         -- do nothing
1433     END;
1434     BEGIN
1435                 -- transfer folders the same way we transfer buckets (see above)
1436                 FOR folder_row in
1437                         SELECT id, name
1438                         FROM   reporter.output_folder
1439                         WHERE  owner = src_usr
1440                 LOOP
1441                         suffix := ' (' || src_usr || ')';
1442                         LOOP
1443                                 BEGIN
1444                                         UPDATE  reporter.output_folder
1445                                         SET     owner = dest_usr, name = name || suffix
1446                                         WHERE   id = folder_row.id;
1447                                 EXCEPTION WHEN unique_violation THEN
1448                                         suffix := suffix || ' ';
1449                                         CONTINUE;
1450                                 END;
1451                                 EXIT;
1452                         END LOOP;
1453                 END LOOP;
1454     EXCEPTION WHEN undefined_table THEN
1455         -- do nothing
1456     END;
1457
1458     -- Finally, delete the source user
1459     DELETE FROM actor.usr WHERE id = src_usr;
1460
1461 END;
1462 $$ LANGUAGE plpgsql;
1463
1464
1465
1466
1467
1468 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1056', :eg_version); -- miker/gmcharlt
1469
1470 INSERT INTO permission.perm_list (id,code,description) VALUES (592,'CONTAINER_BATCH_UPDATE','Allow batch update via buckets');
1471
1472 INSERT INTO container.user_bucket_type (code,label) SELECT code,label FROM container.copy_bucket_type where code = 'staff_client';
1473
1474 CREATE TABLE action.fieldset_group (
1475     id              SERIAL  PRIMARY KEY,
1476     name            TEXT        NOT NULL,
1477     create_time     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1478     complete_time   TIMESTAMPTZ,
1479     container       INT,        -- Points to a container of some type ...
1480     container_type  TEXT,       -- One of 'biblio_record_entry', 'user', 'call_number', 'copy'
1481     can_rollback    BOOL        DEFAULT TRUE,
1482     rollback_group  INT         REFERENCES action.fieldset_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1483     rollback_time   TIMESTAMPTZ,
1484     creator         INT         NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1485     owning_lib      INT         NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
1486 );
1487
1488 ALTER TABLE action.fieldset ADD COLUMN fieldset_group INT REFERENCES action.fieldset_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1489 ALTER TABLE action.fieldset ADD COLUMN error_msg TEXT;
1490 ALTER TABLE container.biblio_record_entry_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1491 ALTER TABLE container.user_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1492 ALTER TABLE container.call_number_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1493 ALTER TABLE container.copy_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1494
1495 UPDATE query.stored_query SET id = id + 1000 WHERE id < 1000;
1496 UPDATE query.from_relation SET id = id + 1000 WHERE id < 1000;
1497 UPDATE query.expression SET id = id + 1000 WHERE id < 1000;
1498
1499 SELECT SETVAL('query.stored_query_id_seq', 1, FALSE);
1500 SELECT SETVAL('query.from_relation_id_seq', 1, FALSE);
1501 SELECT SETVAL('query.expression_id_seq', 1, FALSE);
1502
1503 INSERT INTO query.bind_variable (name,type,description,label)
1504     SELECT  'bucket','number','ID of the bucket to pull items from','Bucket ID'
1505       WHERE NOT EXISTS (SELECT 1 FROM query.bind_variable WHERE name = 'bucket');
1506
1507 -- Assumes completely empty 'query' schema
1508 INSERT INTO query.stored_query (type, use_distinct) VALUES ('SELECT', TRUE); -- 1
1509
1510 INSERT INTO query.from_relation (type, table_name, class_name, table_alias) VALUES ('RELATION', 'container.user_bucket_item', 'cubi', 'cubi'); -- 1
1511 UPDATE query.stored_query SET from_clause = 1;
1512
1513 INSERT INTO query.expr_xcol (table_alias, column_name) VALUES ('cubi', 'target_user'); -- 1
1514 INSERT INTO query.select_item (stored_query,seq_no,expression) VALUES (1,1,1);
1515
1516 INSERT INTO query.expr_xcol (table_alias, column_name) VALUES ('cubi', 'bucket'); -- 2
1517 INSERT INTO query.expr_xbind (bind_variable) VALUES ('bucket'); -- 3
1518
1519 INSERT INTO query.expr_xop (left_operand, operator, right_operand) VALUES (2, '=', 3); -- 4
1520 UPDATE query.stored_query SET where_clause = 4;
1521
1522 SELECT SETVAL('query.stored_query_id_seq', 1000, TRUE) FROM query.stored_query;
1523 SELECT SETVAL('query.from_relation_id_seq', 1000, TRUE) FROM query.from_relation;
1524 SELECT SETVAL('query.expression_id_seq', 10000, TRUE) FROM query.expression;
1525
1526 CREATE OR REPLACE FUNCTION action.apply_fieldset(
1527     fieldset_id IN INT,        -- id from action.fieldset
1528     table_name  IN TEXT,       -- table to be updated
1529     pkey_name   IN TEXT,       -- name of primary key column in that table
1530     query       IN TEXT        -- query constructed by qstore (for query-based
1531                                --    fieldsets only; otherwise null
1532 )
1533 RETURNS TEXT AS $$
1534 DECLARE
1535     statement TEXT;
1536     where_clause TEXT;
1537     fs_status TEXT;
1538     fs_pkey_value TEXT;
1539     fs_query TEXT;
1540     sep CHAR;
1541     status_code TEXT;
1542     msg TEXT;
1543     fs_id INT;
1544     fsg_id INT;
1545     update_count INT;
1546     cv RECORD;
1547     fs_obj action.fieldset%ROWTYPE;
1548     fs_group action.fieldset_group%ROWTYPE;
1549     rb_row RECORD;
1550 BEGIN
1551     -- Sanity checks
1552     IF fieldset_id IS NULL THEN
1553         RETURN 'Fieldset ID parameter is NULL';
1554     END IF;
1555     IF table_name IS NULL THEN
1556         RETURN 'Table name parameter is NULL';
1557     END IF;
1558     IF pkey_name IS NULL THEN
1559         RETURN 'Primary key name parameter is NULL';
1560     END IF;
1561
1562     SELECT
1563         status,
1564         quote_literal( pkey_value )
1565     INTO
1566         fs_status,
1567         fs_pkey_value
1568     FROM
1569         action.fieldset
1570     WHERE
1571         id = fieldset_id;
1572
1573     --
1574     -- Build the WHERE clause.  This differs according to whether it's a
1575     -- single-row fieldset or a query-based fieldset.
1576     --
1577     IF query IS NULL        AND fs_pkey_value IS NULL THEN
1578         RETURN 'Incomplete fieldset: neither a primary key nor a query available';
1579     ELSIF query IS NOT NULL AND fs_pkey_value IS NULL THEN
1580         fs_query := rtrim( query, ';' );
1581         where_clause := 'WHERE ' || pkey_name || ' IN ( '
1582                      || fs_query || ' )';
1583     ELSIF query IS NULL     AND fs_pkey_value IS NOT NULL THEN
1584         where_clause := 'WHERE ' || pkey_name || ' = ';
1585         IF pkey_name = 'id' THEN
1586             where_clause := where_clause || fs_pkey_value;
1587         ELSIF pkey_name = 'code' THEN
1588             where_clause := where_clause || quote_literal(fs_pkey_value);
1589         ELSE
1590             RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
1591         END IF;
1592     ELSE  -- both are not null
1593         RETURN 'Ambiguous fieldset: both a primary key and a query provided';
1594     END IF;
1595
1596     IF fs_status IS NULL THEN
1597         RETURN 'No fieldset found for id = ' || fieldset_id;
1598     ELSIF fs_status = 'APPLIED' THEN
1599         RETURN 'Fieldset ' || fieldset_id || ' has already been applied';
1600     END IF;
1601
1602     SELECT * INTO fs_obj FROM action.fieldset WHERE id = fieldset_id;
1603     SELECT * INTO fs_group FROM action.fieldset_group WHERE id = fs_obj.fieldset_group;
1604
1605     IF fs_group.can_rollback THEN
1606         -- This is part of a non-rollback group.  We need to record the current values for future rollback.
1607
1608         INSERT INTO action.fieldset_group (can_rollback, name, creator, owning_lib, container, container_type)
1609             VALUES (FALSE, 'ROLLBACK: '|| fs_group.name, fs_group.creator, fs_group.owning_lib, fs_group.container, fs_group.container_type);
1610
1611         fsg_id := CURRVAL('action.fieldset_group_id_seq');
1612
1613         FOR rb_row IN EXECUTE 'SELECT * FROM ' || table_name || ' ' || where_clause LOOP
1614             IF pkey_name = 'id' THEN
1615                 fs_pkey_value := rb_row.id;
1616             ELSIF pkey_name = 'code' THEN
1617                 fs_pkey_value := rb_row.code;
1618             ELSE
1619                 RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
1620             END IF;
1621             INSERT INTO action.fieldset (fieldset_group,owner,owning_lib,status,classname,name,pkey_value)
1622                 VALUES (fsg_id, fs_obj.owner, fs_obj.owning_lib, 'PENDING', fs_obj.classname, fs_obj.name || ' ROLLBACK FOR ' || fs_pkey_value, fs_pkey_value);
1623
1624             fs_id := CURRVAL('action.fieldset_id_seq');
1625             sep := '';
1626             FOR cv IN
1627                 SELECT  DISTINCT col
1628                 FROM    action.fieldset_col_val
1629                 WHERE   fieldset = fieldset_id
1630             LOOP
1631                 EXECUTE 'INSERT INTO action.fieldset_col_val (fieldset, col, val) ' || 
1632                     'SELECT '|| fs_id || ', '||quote_literal(cv.col)||', '||cv.col||' FROM '||table_name||' WHERE '||pkey_name||' = '||fs_pkey_value;
1633             END LOOP;
1634         END LOOP;
1635     END IF;
1636
1637     statement := 'UPDATE ' || table_name || ' SET';
1638
1639     sep := '';
1640     FOR cv IN
1641         SELECT  col,
1642                 val
1643         FROM    action.fieldset_col_val
1644         WHERE   fieldset = fieldset_id
1645     LOOP
1646         statement := statement || sep || ' ' || cv.col
1647                      || ' = ' || coalesce( quote_literal( cv.val ), 'NULL' );
1648         sep := ',';
1649     END LOOP;
1650
1651     IF sep = '' THEN
1652         RETURN 'Fieldset ' || fieldset_id || ' has no column values defined';
1653     END IF;
1654     statement := statement || ' ' || where_clause;
1655
1656     --
1657     -- Execute the update
1658     --
1659     BEGIN
1660         EXECUTE statement;
1661         GET DIAGNOSTICS update_count = ROW_COUNT;
1662
1663         IF update_count = 0 THEN
1664             RAISE data_exception;
1665         END IF;
1666
1667         IF fsg_id IS NOT NULL THEN
1668             UPDATE action.fieldset_group SET rollback_group = fsg_id WHERE id = fs_group.id;
1669         END IF;
1670
1671         IF fs_group.id IS NOT NULL THEN
1672             UPDATE action.fieldset_group SET complete_time = now() WHERE id = fs_group.id;
1673         END IF;
1674
1675         UPDATE action.fieldset SET status = 'APPLIED', applied_time = now() WHERE id = fieldset_id;
1676
1677     EXCEPTION WHEN data_exception THEN
1678         msg := 'No eligible rows found for fieldset ' || fieldset_id;
1679         UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
1680         RETURN msg;
1681
1682     END;
1683
1684     RETURN msg;
1685
1686 EXCEPTION WHEN OTHERS THEN
1687     msg := 'Unable to apply fieldset ' || fieldset_id || ': ' || sqlerrm;
1688     UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
1689     RETURN msg;
1690
1691 END;
1692 $$ LANGUAGE plpgsql;
1693
1694
1695
1696 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1057', :eg_version); -- miker/gmcharlt/kmlussier
1697
1698 -- Thist change drops a needless join and saves 10-15% in time cost
1699 CREATE OR REPLACE FUNCTION search.facets_for_record_set(ignore_facet_classes text[], hits bigint[]) RETURNS TABLE(id integer, value text, count bigint)
1700 AS $f$
1701     SELECT id, value, count
1702       FROM (
1703         SELECT  mfae.field AS id,
1704                 mfae.value,
1705                 COUNT(DISTINCT mfae.source),
1706                 row_number() OVER (
1707                     PARTITION BY mfae.field ORDER BY COUNT(DISTINCT mfae.source) DESC
1708                 ) AS rownum
1709           FROM  metabib.facet_entry mfae
1710                 JOIN config.metabib_field cmf ON (cmf.id = mfae.field)
1711           WHERE mfae.source = ANY ($2)
1712                 AND cmf.facet_field
1713                 AND cmf.field_class NOT IN (SELECT * FROM unnest($1))
1714           GROUP by 1, 2
1715       ) all_facets
1716       WHERE rownum <= (
1717         SELECT COALESCE(
1718             (SELECT value::INT FROM config.global_flag WHERE name = 'search.max_facets_per_field' AND enabled),
1719             1000
1720         )
1721       );
1722 $f$ LANGUAGE SQL;
1723
1724 CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$
1725 DECLARE
1726     layout          unapi.bre_output_layout%ROWTYPE;
1727     transform       config.xml_transform%ROWTYPE;
1728     item_format     TEXT;
1729     tmp_xml         TEXT;
1730     xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
1731     ouid            INT;
1732     element_list    TEXT[];
1733 BEGIN
1734
1735     IF org = '-' OR org IS NULL THEN
1736         SELECT shortname INTO org FROM evergreen.org_top();
1737     END IF;
1738
1739     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
1740     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
1741
1742     IF layout.name IS NULL THEN
1743         RETURN NULL::XML;
1744     END IF;
1745
1746     SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
1747     xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
1748
1749     -- Gather the bib xml
1750     SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
1751
1752     IF layout.title_element IS NOT NULL THEN
1753         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
1754     END IF;
1755
1756     IF layout.description_element IS NOT NULL THEN
1757         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
1758     END IF;
1759
1760     IF layout.creator_element IS NOT NULL THEN
1761         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
1762     END IF;
1763
1764     IF layout.update_ts_element IS NOT NULL THEN
1765         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts;
1766     END IF;
1767
1768     IF unapi_url IS NOT NULL THEN
1769         EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
1770     END IF;
1771
1772     IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
1773
1774     element_list := regexp_split_to_array(layout.feed_top,E'\\.');
1775     FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
1776         EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
1777     END LOOP;
1778
1779     RETURN tmp_xml::XML;
1780 END;
1781 $F$ LANGUAGE PLPGSQL STABLE;
1782
1783 CREATE TABLE asset.copy_vis_attr_cache (
1784     id              BIGSERIAL   PRIMARY KEY,
1785     record          BIGINT      NOT NULL, -- No FKEYs, managed by user triggers.
1786     target_copy     BIGINT      NOT NULL,
1787     vis_attr_vector INT[]
1788 );
1789 CREATE INDEX copy_vis_attr_cache_record_idx ON asset.copy_vis_attr_cache (record);
1790 CREATE INDEX copy_vis_attr_cache_copy_idx ON asset.copy_vis_attr_cache (target_copy);
1791
1792 ALTER TABLE biblio.record_entry ADD COLUMN vis_attr_vector INT[];
1793
1794 CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute ( value INT, attr TEXT ) RETURNS INT AS $f$
1795 SELECT  ((CASE $2
1796
1797             WHEN 'luri_org'         THEN 0 -- "b" attr
1798             WHEN 'bib_source'       THEN 1 -- "b" attr
1799
1800             WHEN 'copy_flags'       THEN 0 -- "c" attr
1801             WHEN 'owning_lib'       THEN 1 -- "c" attr
1802             WHEN 'circ_lib'         THEN 2 -- "c" attr
1803             WHEN 'status'           THEN 3 -- "c" attr
1804             WHEN 'location'         THEN 4 -- "c" attr
1805             WHEN 'location_group'   THEN 5 -- "c" attr
1806
1807         END) << 28 ) | $1;
1808
1809 /* copy_flags bit positions, LSB-first:
1810
1811  0: asset.copy.opac_visible
1812
1813
1814    When adding flags, you must update asset.all_visible_flags()
1815
1816    Because bib and copy values are stored separately, we can reuse
1817    shifts, saving us some space. We could probably take back a bit
1818    too, but I'm not sure its worth squeezing that last one out. We'd
1819    be left with just 2 slots for copy attrs, rather than 10.
1820 */
1821
1822 $f$ LANGUAGE SQL IMMUTABLE;
1823
1824 CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_list ( attr TEXT, value INT[] ) RETURNS INT[] AS $f$
1825     SELECT ARRAY_AGG(search.calculate_visibility_attribute(x, $1)) FROM UNNEST($2) AS X;
1826 $f$ LANGUAGE SQL IMMUTABLE;
1827
1828 CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_test ( attr TEXT, value INT[], negate BOOL DEFAULT FALSE ) RETURNS TEXT AS $f$
1829     SELECT  CASE WHEN $3 THEN '!' ELSE '' END || '(' || ARRAY_TO_STRING(search.calculate_visibility_attribute_list($1,$2),'|') || ')';
1830 $f$ LANGUAGE SQL IMMUTABLE;
1831
1832 CREATE OR REPLACE FUNCTION asset.calculate_copy_visibility_attribute_set ( copy_id BIGINT ) RETURNS INT[] AS $f$
1833 DECLARE
1834     copy_row    asset.copy%ROWTYPE;
1835     lgroup_map  asset.copy_location_group_map%ROWTYPE;
1836     attr_set    INT[];
1837 BEGIN
1838     SELECT * INTO copy_row FROM asset.copy WHERE id = copy_id;
1839
1840     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.opac_visible::INT, 'copy_flags');
1841     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.circ_lib, 'circ_lib');
1842     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.status, 'status');
1843     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.location, 'location');
1844
1845     SELECT  ARRAY_APPEND(
1846                 attr_set,
1847                 search.calculate_visibility_attribute(owning_lib, 'owning_lib')
1848             ) INTO attr_set
1849       FROM  asset.call_number
1850       WHERE id = copy_row.call_number;
1851
1852     FOR lgroup_map IN SELECT * FROM asset.copy_location_group_map WHERE location = copy_row.location LOOP
1853         attr_set := attr_set || search.calculate_visibility_attribute(lgroup_map.lgroup, 'location_group');
1854     END LOOP;
1855
1856     RETURN attr_set;
1857 END;
1858 $f$ LANGUAGE PLPGSQL;
1859
1860 CREATE OR REPLACE FUNCTION biblio.calculate_bib_visibility_attribute_set ( bib_id BIGINT ) RETURNS INT[] AS $f$
1861 DECLARE
1862     bib_row     biblio.record_entry%ROWTYPE;
1863     cn_row      asset.call_number%ROWTYPE;
1864     attr_set    INT[];
1865 BEGIN
1866     SELECT * INTO bib_row FROM biblio.record_entry WHERE id = bib_id;
1867
1868     IF bib_row.source IS NOT NULL THEN
1869         attr_set := attr_set || search.calculate_visibility_attribute(bib_row.source, 'bib_source');
1870     END IF;
1871
1872     FOR cn_row IN
1873         SELECT  cn.*
1874           FROM  asset.call_number cn
1875                 JOIN asset.uri_call_number_map m ON (cn.id = m.call_number)
1876                 JOIN asset.uri u ON (u.id = m.uri)
1877           WHERE cn.record = bib_id
1878                 AND cn.label = '##URI##'
1879                 AND u.active
1880     LOOP
1881         attr_set := attr_set || search.calculate_visibility_attribute(cn_row.owning_lib, 'luri_org');
1882     END LOOP;
1883
1884     RETURN attr_set;
1885 END;
1886 $f$ LANGUAGE PLPGSQL;
1887
1888
1889 SELECT evergreen.upgrade_deps_block_check('1076', :eg_version); -- miker/gmcharlt
1890
1891 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
1892 DECLARE
1893     ocn     asset.call_number%ROWTYPE;
1894     ncn     asset.call_number%ROWTYPE;
1895     cid     BIGINT;
1896 BEGIN
1897
1898     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN -- Only needs ON INSERT OR DELETE, so handle separately
1899         IF TG_OP = 'INSERT' THEN
1900             INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
1901                 NEW.peer_record,
1902                 NEW.target_copy,
1903                 asset.calculate_copy_visibility_attribute_set(NEW.target_copy)
1904             );
1905
1906             RETURN NEW;
1907         ELSIF TG_OP = 'DELETE' THEN
1908             DELETE FROM asset.copy_vis_attr_cache
1909               WHERE record = NEW.peer_record AND target_copy = NEW.target_copy;
1910
1911             RETURN OLD;
1912         END IF;
1913     END IF;
1914
1915     IF TG_OP = 'INSERT' THEN -- Handles ON INSERT. ON UPDATE is below.
1916         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
1917             SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
1918             INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
1919                 ncn.record,
1920                 NEW.id,
1921                 asset.calculate_copy_visibility_attribute_set(NEW.id)
1922             );
1923         ELSIF TG_TABLE_NAME = 'record_entry' THEN
1924             NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
1925         END IF;
1926
1927         RETURN NEW;
1928     END IF;
1929
1930     -- handle items first, since with circulation activity
1931     -- their statuses change frequently
1932     IF TG_TABLE_NAME IN ('copy', 'unit') THEN -- This handles ON UPDATE OR DELETE. ON INSERT above
1933
1934         IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
1935             DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
1936             RETURN OLD;
1937         END IF;
1938
1939         SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
1940
1941         IF OLD.deleted <> NEW.deleted THEN
1942             IF NEW.deleted THEN
1943                 DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
1944             ELSE
1945                 INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
1946                     ncn.record,
1947                     NEW.id,
1948                     asset.calculate_copy_visibility_attribute_set(NEW.id)
1949                 );
1950             END IF;
1951
1952             RETURN NEW;
1953         ELSIF OLD.call_number  <> NEW.call_number THEN
1954             SELECT * INTO ocn FROM asset.call_number cn WHERE id = OLD.call_number;
1955
1956             IF ncn.record <> ocn.record THEN
1957                 UPDATE  biblio.record_entry
1958                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(ncn.record)
1959                   WHERE id = ocn.record;
1960
1961                 -- We have to use a record-specific WHERE clause
1962                 -- to avoid modifying the entries for peer-bib copies.
1963                 UPDATE  asset.copy_vis_attr_cache
1964                   SET   target_copy = NEW.id,
1965                         record = ncn.record
1966                   WHERE target_copy = OLD.id
1967                         AND record = ocn.record;
1968             END IF;
1969         END IF;
1970
1971         IF OLD.location     <> NEW.location OR
1972            OLD.status       <> NEW.status OR
1973            OLD.opac_visible <> NEW.opac_visible OR
1974            OLD.circ_lib     <> NEW.circ_lib
1975         THEN
1976             -- Any of these could change visibility, but
1977             -- we'll save some queries and not try to calculate
1978             -- the change directly.  We want to update peer-bib
1979             -- entries in this case, unlike above.
1980             UPDATE  asset.copy_vis_attr_cache
1981               SET   target_copy = NEW.id,
1982                     vis_attr_vector = asset.calculate_copy_visibility_attribute_set(NEW.id)
1983               WHERE target_copy = OLD.id;
1984
1985         END IF;
1986
1987     ELSIF TG_TABLE_NAME = 'call_number' THEN -- Only ON UPDATE. Copy handler will deal with ON INSERT OR DELETE.
1988
1989         IF OLD.record <> NEW.record THEN
1990             IF NEW.label = '##URI##' THEN
1991                 UPDATE  biblio.record_entry
1992                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
1993                   WHERE id = OLD.record;
1994
1995                 UPDATE  biblio.record_entry
1996                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(NEW.record)
1997                   WHERE id = NEW.record;
1998             END IF;
1999
2000             UPDATE  asset.copy_vis_attr_cache
2001               SET   record = NEW.record,
2002                     vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
2003               WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
2004                     AND record = OLD.record;
2005
2006         ELSIF OLD.owning_lib <> NEW.owning_lib THEN
2007             UPDATE  asset.copy_vis_attr_cache
2008               SET   vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
2009               WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
2010                     AND record = NEW.record;
2011
2012             IF NEW.label = '##URI##' THEN
2013                 UPDATE  biblio.record_entry
2014                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
2015                   WHERE id = OLD.record;
2016             END IF;
2017         END IF;
2018
2019     ELSIF TG_TABLE_NAME = 'record_entry' THEN -- Only handles ON UPDATE OR DELETE
2020
2021         IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
2022             DELETE FROM asset.copy_vis_attr_cache WHERE record = OLD.id;
2023             RETURN OLD;
2024         ELSIF OLD.source <> NEW.source THEN
2025             NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
2026         END IF;
2027
2028     END IF;
2029
2030     RETURN NEW;
2031 END;
2032 $func$ LANGUAGE PLPGSQL;
2033
2034
2035 -- Helper functions for use in constructing searches --
2036
2037 CREATE OR REPLACE FUNCTION asset.all_visible_flags () RETURNS TEXT AS $f$
2038     SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(1 << x, 'copy_flags')),'&') || ')'
2039       FROM  GENERATE_SERIES(0,0) AS x; -- increment as new flags are added.
2040 $f$ LANGUAGE SQL STABLE;
2041
2042 CREATE OR REPLACE FUNCTION asset.visible_orgs (otype TEXT) RETURNS TEXT AS $f$
2043     SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
2044       FROM  actor.org_unit
2045       WHERE opac_visible;
2046 $f$ LANGUAGE SQL STABLE;
2047
2048 CREATE OR REPLACE FUNCTION asset.invisible_orgs (otype TEXT) RETURNS TEXT AS $f$
2049     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
2050       FROM  actor.org_unit
2051       WHERE NOT opac_visible;
2052 $f$ LANGUAGE SQL STABLE;
2053
2054 -- Bib-oriented defaults for search
2055 CREATE OR REPLACE FUNCTION asset.bib_source_default () RETURNS TEXT AS $f$
2056     SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'bib_source')),'|') || ')'
2057       FROM  config.bib_source
2058       WHERE transcendant;
2059 $f$ LANGUAGE SQL IMMUTABLE;
2060
2061 CREATE OR REPLACE FUNCTION asset.luri_org_default () RETURNS TEXT AS $f$
2062     SELECT  * FROM asset.invisible_orgs('luri_org');
2063 $f$ LANGUAGE SQL STABLE;
2064
2065 -- Copy-oriented defaults for search
2066 CREATE OR REPLACE FUNCTION asset.location_group_default () RETURNS TEXT AS $f$
2067     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location_group')),'|') || ')'
2068       FROM  asset.copy_location_group
2069       WHERE NOT opac_visible;
2070 $f$ LANGUAGE SQL STABLE;
2071
2072 CREATE OR REPLACE FUNCTION asset.location_default () RETURNS TEXT AS $f$
2073     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location')),'|') || ')'
2074       FROM  asset.copy_location
2075       WHERE NOT opac_visible;
2076 $f$ LANGUAGE SQL STABLE;
2077
2078 CREATE OR REPLACE FUNCTION asset.status_default () RETURNS TEXT AS $f$
2079     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'status')),'|') || ')'
2080       FROM  config.copy_status
2081       WHERE NOT opac_visible;
2082 $f$ LANGUAGE SQL STABLE;
2083
2084 CREATE OR REPLACE FUNCTION asset.owning_lib_default () RETURNS TEXT AS $f$
2085     SELECT  * FROM asset.invisible_orgs('owning_lib');
2086 $f$ LANGUAGE SQL STABLE;
2087
2088 CREATE OR REPLACE FUNCTION asset.circ_lib_default () RETURNS TEXT AS $f$
2089     SELECT  * FROM asset.invisible_orgs('circ_lib');
2090 $f$ LANGUAGE SQL STABLE;
2091
2092 CREATE OR REPLACE FUNCTION asset.patron_default_visibility_mask () RETURNS TABLE (b_attrs TEXT, c_attrs TEXT)  AS $f$
2093 DECLARE
2094     copy_flags      TEXT; -- "c" attr
2095
2096     owning_lib      TEXT; -- "c" attr
2097     circ_lib        TEXT; -- "c" attr
2098     status          TEXT; -- "c" attr
2099     location        TEXT; -- "c" attr
2100     location_group  TEXT; -- "c" attr
2101
2102     luri_org        TEXT; -- "b" attr
2103     bib_sources     TEXT; -- "b" attr
2104 BEGIN
2105     copy_flags      := asset.all_visible_flags(); -- Will always have at least one
2106
2107     owning_lib      := NULLIF(asset.owning_lib_default(),'!()');
2108     
2109     circ_lib        := NULLIF(asset.circ_lib_default(),'!()');
2110     status          := NULLIF(asset.status_default(),'!()');
2111     location        := NULLIF(asset.location_default(),'!()');
2112     location_group  := NULLIF(asset.location_group_default(),'!()');
2113
2114     luri_org        := NULLIF(asset.luri_org_default(),'!()');
2115     bib_sources     := NULLIF(asset.bib_source_default(),'()');
2116
2117     RETURN QUERY SELECT
2118         '('||ARRAY_TO_STRING(
2119             ARRAY[luri_org,bib_sources],
2120             '|'
2121         )||')',
2122         '('||ARRAY_TO_STRING(
2123             ARRAY[copy_flags,owning_lib,circ_lib,status,location,location_group]::TEXT[],
2124             '&'
2125         )||')';
2126 END;
2127 $f$ LANGUAGE PLPGSQL STABLE ROWS 1;
2128
2129 CREATE OR REPLACE FUNCTION metabib.suggest_browse_entries(raw_query_text text, search_class text, headline_opts text, visibility_org integer, query_limit integer, normalization integer)
2130  RETURNS TABLE(value text, field integer, buoyant_and_class_match boolean, field_match boolean, field_weight integer, rank real, buoyant boolean, match text)
2131 AS $f$
2132 DECLARE
2133     prepared_query_texts    TEXT[];
2134     query                   TSQUERY;
2135     plain_query             TSQUERY;
2136     opac_visibility_join    TEXT;
2137     search_class_join       TEXT;
2138     r_fields                RECORD;
2139 BEGIN
2140     prepared_query_texts := metabib.autosuggest_prepare_tsquery(raw_query_text);
2141
2142     query := TO_TSQUERY('keyword', prepared_query_texts[1]);
2143     plain_query := TO_TSQUERY('keyword', prepared_query_texts[2]);
2144
2145     visibility_org := NULLIF(visibility_org,-1);
2146     IF visibility_org IS NOT NULL THEN
2147         PERFORM FROM actor.org_unit WHERE id = visibility_org AND parent_ou IS NULL;
2148         IF FOUND THEN
2149             opac_visibility_join := '';
2150         ELSE
2151             opac_visibility_join := '
2152     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = x.source)
2153     JOIN vm ON (acvac.vis_attr_vector @@
2154             (vm.c_attrs || $$&$$ ||
2155                 search.calculate_visibility_attribute_test(
2156                     $$circ_lib$$,
2157                     (SELECT ARRAY_AGG(id) FROM actor.org_unit_descendants($4))
2158                 )
2159             )::query_int
2160          )
2161 ';
2162         END IF;
2163     ELSE
2164         opac_visibility_join := '';
2165     END IF;
2166
2167     -- The following determines whether we only provide suggestsons matching
2168     -- the user's selected search_class, or whether we show other suggestions
2169     -- too. The reason for MIN() is that for search_classes like
2170     -- 'title|proper|uniform' you would otherwise get multiple rows.  The
2171     -- implication is that if title as a class doesn't have restrict,
2172     -- nor does the proper field, but the uniform field does, you're going
2173     -- to get 'false' for your overall evaluation of 'should we restrict?'
2174     -- To invert that, change from MIN() to MAX().
2175
2176     SELECT
2177         INTO r_fields
2178             MIN(cmc.restrict::INT) AS restrict_class,
2179             MIN(cmf.restrict::INT) AS restrict_field
2180         FROM metabib.search_class_to_registered_components(search_class)
2181             AS _registered (field_class TEXT, field INT)
2182         JOIN
2183             config.metabib_class cmc ON (cmc.name = _registered.field_class)
2184         LEFT JOIN
2185             config.metabib_field cmf ON (cmf.id = _registered.field);
2186
2187     -- evaluate 'should we restrict?'
2188     IF r_fields.restrict_field::BOOL OR r_fields.restrict_class::BOOL THEN
2189         search_class_join := '
2190     JOIN
2191         metabib.search_class_to_registered_components($2)
2192         AS _registered (field_class TEXT, field INT) ON (
2193             (_registered.field IS NULL AND
2194                 _registered.field_class = cmf.field_class) OR
2195             (_registered.field = cmf.id)
2196         )
2197     ';
2198     ELSE
2199         search_class_join := '
2200     LEFT JOIN
2201         metabib.search_class_to_registered_components($2)
2202         AS _registered (field_class TEXT, field INT) ON (
2203             _registered.field_class = cmc.name
2204         )
2205     ';
2206     END IF;
2207
2208     RETURN QUERY EXECUTE '
2209 WITH vm AS ( SELECT * FROM asset.patron_default_visibility_mask() ),
2210      mbe AS (SELECT * FROM metabib.browse_entry WHERE index_vector @@ $1 LIMIT 10000)
2211 SELECT  DISTINCT
2212         x.value,
2213         x.id,
2214         x.push,
2215         x.restrict,
2216         x.weight,
2217         x.ts_rank_cd,
2218         x.buoyant,
2219         TS_HEADLINE(value, $7, $3)
2220   FROM  (SELECT DISTINCT
2221                 mbe.value,
2222                 cmf.id,
2223                 cmc.buoyant AND _registered.field_class IS NOT NULL AS push,
2224                 _registered.field = cmf.id AS restrict,
2225                 cmf.weight,
2226                 TS_RANK_CD(mbe.index_vector, $1, $6),
2227                 cmc.buoyant,
2228                 mbedm.source
2229           FROM  metabib.browse_entry_def_map mbedm
2230                 JOIN mbe ON (mbe.id = mbedm.entry)
2231                 JOIN config.metabib_field cmf ON (cmf.id = mbedm.def)
2232                 JOIN config.metabib_class cmc ON (cmf.field_class = cmc.name)
2233                 '  || search_class_join || '
2234           ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
2235           LIMIT 1000) AS x
2236         ' || opac_visibility_join || '
2237   ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
2238   LIMIT $5
2239 '   -- sic, repeat the order by clause in the outer select too
2240     USING
2241         query, search_class, headline_opts,
2242         visibility_org, query_limit, normalization, plain_query
2243         ;
2244
2245     -- sort order:
2246     --  buoyant AND chosen class = match class
2247     --  chosen field = match field
2248     --  field weight
2249     --  rank
2250     --  buoyancy
2251     --  value itself
2252
2253 END;
2254 $f$ LANGUAGE plpgsql ROWS 10;
2255
2256 CREATE OR REPLACE FUNCTION metabib.browse(search_field integer[], browse_term text, context_org integer DEFAULT NULL::integer, context_loc_group integer DEFAULT NULL::integer, staff boolean DEFAULT false, pivot_id bigint DEFAULT NULL::bigint, result_limit integer DEFAULT 10)
2257  RETURNS SETOF metabib.flat_browse_entry_appearance
2258 AS $f$
2259 DECLARE
2260     core_query              TEXT;
2261     back_query              TEXT;
2262     forward_query           TEXT;
2263     pivot_sort_value        TEXT;
2264     pivot_sort_fallback     TEXT;
2265     context_locations       INT[];
2266     browse_superpage_size   INT;
2267     results_skipped         INT := 0;
2268     back_limit              INT;
2269     back_to_pivot           INT;
2270     forward_limit           INT;
2271     forward_to_pivot        INT;
2272 BEGIN
2273     -- First, find the pivot if we were given a browse term but not a pivot.
2274     IF pivot_id IS NULL THEN
2275         pivot_id := metabib.browse_pivot(search_field, browse_term);
2276     END IF;
2277
2278     SELECT INTO pivot_sort_value, pivot_sort_fallback
2279         sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
2280
2281     -- Bail if we couldn't find a pivot.
2282     IF pivot_sort_value IS NULL THEN
2283         RETURN;
2284     END IF;
2285
2286     -- Transform the context_loc_group argument (if any) (logc at the
2287     -- TPAC layer) into a form we'll be able to use.
2288     IF context_loc_group IS NOT NULL THEN
2289         SELECT INTO context_locations ARRAY_AGG(location)
2290             FROM asset.copy_location_group_map
2291             WHERE lgroup = context_loc_group;
2292     END IF;
2293
2294     -- Get the configured size of browse superpages.
2295     SELECT INTO browse_superpage_size COALESCE(value::INT,100)     -- NULL ok
2296         FROM config.global_flag
2297         WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
2298
2299     -- First we're going to search backward from the pivot, then we're going
2300     -- to search forward.  In each direction, we need two limits.  At the
2301     -- lesser of the two limits, we delineate the edge of the result set
2302     -- we're going to return.  At the greater of the two limits, we find the
2303     -- pivot value that would represent an offset from the current pivot
2304     -- at a distance of one "page" in either direction, where a "page" is a
2305     -- result set of the size specified in the "result_limit" argument.
2306     --
2307     -- The two limits in each direction make four derived values in total,
2308     -- and we calculate them now.
2309     back_limit := CEIL(result_limit::FLOAT / 2);
2310     back_to_pivot := result_limit;
2311     forward_limit := result_limit / 2;
2312     forward_to_pivot := result_limit - 1;
2313
2314     -- This is the meat of the SQL query that finds browse entries.  We'll
2315     -- pass this to a function which uses it with a cursor, so that individual
2316     -- rows may be fetched in a loop until some condition is satisfied, without
2317     -- waiting for a result set of fixed size to be collected all at once.
2318     core_query := '
2319 SELECT  mbe.id,
2320         mbe.value,
2321         mbe.sort_value
2322   FROM  metabib.browse_entry mbe
2323   WHERE (
2324             EXISTS ( -- are there any bibs using this mbe via the requested fields?
2325                 SELECT  1
2326                   FROM  metabib.browse_entry_def_map mbedm
2327                   WHERE mbedm.entry = mbe.id AND mbedm.def = ANY(' || quote_literal(search_field) || ')
2328             ) OR EXISTS ( -- are there any authorities using this mbe via the requested fields?
2329                 SELECT  1
2330                   FROM  metabib.browse_entry_simple_heading_map mbeshm
2331                         JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2332                         JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2333                             ash.atag = map.authority_field
2334                             AND map.metabib_field = ANY(' || quote_literal(search_field) || ')
2335                         )
2336                   WHERE mbeshm.entry = mbe.id
2337             )
2338         ) AND ';
2339
2340     -- This is the variant of the query for browsing backward.
2341     back_query := core_query ||
2342         ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
2343     ' ORDER BY mbe.sort_value DESC, mbe.value DESC LIMIT 1000';
2344
2345     -- This variant browses forward.
2346     forward_query := core_query ||
2347         ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
2348     ' ORDER BY mbe.sort_value, mbe.value LIMIT 1000';
2349
2350     -- We now call the function which applies a cursor to the provided
2351     -- queries, stopping at the appropriate limits and also giving us
2352     -- the next page's pivot.
2353     RETURN QUERY
2354         SELECT * FROM metabib.staged_browse(
2355             back_query, search_field, context_org, context_locations,
2356             staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
2357         ) UNION
2358         SELECT * FROM metabib.staged_browse(
2359             forward_query, search_field, context_org, context_locations,
2360             staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
2361         ) ORDER BY row_number DESC;
2362
2363 END;
2364 $f$ LANGUAGE plpgsql ROWS 10;
2365
2366 CREATE OR REPLACE FUNCTION metabib.staged_browse(query text, fields integer[], context_org integer, context_locations integer[], staff boolean, browse_superpage_size integer, count_up_from_zero boolean, result_limit integer, next_pivot_pos integer)
2367  RETURNS SETOF metabib.flat_browse_entry_appearance
2368 AS $f$
2369 DECLARE
2370     curs                    REFCURSOR;
2371     rec                     RECORD;
2372     qpfts_query             TEXT;
2373     aqpfts_query            TEXT;
2374     afields                 INT[];
2375     bfields                 INT[];
2376     result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
2377     results_skipped         INT := 0;
2378     row_counter             INT := 0;
2379     row_number              INT;
2380     slice_start             INT;
2381     slice_end               INT;
2382     full_end                INT;
2383     all_records             BIGINT[];
2384     all_brecords             BIGINT[];
2385     all_arecords            BIGINT[];
2386     superpage_of_records    BIGINT[];
2387     superpage_size          INT;
2388     c_tests                 TEXT := '';
2389     b_tests                 TEXT := '';
2390     c_orgs                  INT[];
2391 BEGIN
2392     IF count_up_from_zero THEN
2393         row_number := 0;
2394     ELSE
2395         row_number := -1;
2396     END IF;
2397
2398     IF NOT staff THEN
2399         SELECT x.c_attrs, x.b_attrs INTO c_tests, b_tests FROM asset.patron_default_visibility_mask() x;
2400     END IF;
2401
2402     IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
2403     IF b_tests <> '' THEN b_tests := b_tests || '&'; END IF;
2404
2405     SELECT ARRAY_AGG(id) INTO c_orgs FROM actor.org_unit_descendants(context_org);
2406     
2407     c_tests := c_tests || search.calculate_visibility_attribute_test('circ_lib',c_orgs)
2408                || '&' || search.calculate_visibility_attribute_test('owning_lib',c_orgs);
2409     
2410     PERFORM 1 FROM config.internal_flag WHERE enabled AND name = 'opac.located_uri.act_as_copy';
2411     IF FOUND THEN
2412         b_tests := b_tests || search.calculate_visibility_attribute_test(
2413             'luri_org',
2414             (SELECT ARRAY_AGG(id) FROM actor.org_unit_full_path(context_org) x)
2415         );
2416     ELSE
2417         b_tests := b_tests || search.calculate_visibility_attribute_test(
2418             'luri_org',
2419             (SELECT ARRAY_AGG(id) FROM actor.org_unit_ancestors(context_org) x)
2420         );
2421     END IF;
2422
2423     IF context_locations THEN
2424         IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
2425         c_tests := c_tests || search.calculate_visibility_attribute_test('location',context_locations);
2426     END IF;
2427
2428     OPEN curs NO SCROLL FOR EXECUTE query;
2429
2430     LOOP
2431         FETCH curs INTO rec;
2432         IF NOT FOUND THEN
2433             IF result_row.pivot_point IS NOT NULL THEN
2434                 RETURN NEXT result_row;
2435             END IF;
2436             RETURN;
2437         END IF;
2438
2439         -- Gather aggregate data based on the MBE row we're looking at now, authority axis
2440         SELECT INTO all_arecords, result_row.sees, afields
2441                 ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
2442                 STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
2443                 ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
2444
2445           FROM  metabib.browse_entry_simple_heading_map mbeshm
2446                 JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2447                 JOIN authority.authority_linking aal ON ( ash.record = aal.source )
2448                 JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
2449                 JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2450                     ash.atag = map.authority_field
2451                     AND map.metabib_field = ANY(fields)
2452                 )
2453           WHERE mbeshm.entry = rec.id;
2454
2455         -- Gather aggregate data based on the MBE row we're looking at now, bib axis
2456         SELECT INTO all_brecords, result_row.authorities, bfields
2457                 ARRAY_AGG(DISTINCT source),
2458                 STRING_AGG(DISTINCT authority::TEXT, $$,$$),
2459                 ARRAY_AGG(DISTINCT def)
2460           FROM  metabib.browse_entry_def_map
2461           WHERE entry = rec.id
2462                 AND def = ANY(fields);
2463
2464         SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
2465
2466         result_row.sources := 0;
2467         result_row.asources := 0;
2468
2469         -- Bib-linked vis checking
2470         IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
2471
2472             SELECT  INTO result_row.sources COUNT(DISTINCT b.id)
2473               FROM  biblio.record_entry b
2474                     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
2475               WHERE b.id = ANY(all_brecords[1:browse_superpage_size])
2476                     AND (
2477                         acvac.vis_attr_vector @@ c_tests::query_int
2478                         OR b.vis_attr_vector @@ b_tests::query_int
2479                     );
2480
2481             result_row.accurate := TRUE;
2482
2483         END IF;
2484
2485         -- Authority-linked vis checking
2486         IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
2487
2488             SELECT  INTO result_row.asources COUNT(DISTINCT b.id)
2489               FROM  biblio.record_entry b
2490                     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
2491               WHERE b.id = ANY(all_arecords[1:browse_superpage_size])
2492                     AND (
2493                         acvac.vis_attr_vector @@ c_tests::query_int
2494                         OR b.vis_attr_vector @@ b_tests::query_int
2495                     );
2496
2497             result_row.aaccurate := TRUE;
2498
2499         END IF;
2500
2501         IF result_row.sources > 0 OR result_row.asources > 0 THEN
2502
2503             -- The function that calls this function needs row_number in order
2504             -- to correctly order results from two different runs of this
2505             -- functions.
2506             result_row.row_number := row_number;
2507
2508             -- Now, if row_counter is still less than limit, return a row.  If
2509             -- not, but it is less than next_pivot_pos, continue on without
2510             -- returning actual result rows until we find
2511             -- that next pivot, and return it.
2512
2513             IF row_counter < result_limit THEN
2514                 result_row.browse_entry := rec.id;
2515                 result_row.value := rec.value;
2516
2517                 RETURN NEXT result_row;
2518             ELSE
2519                 result_row.browse_entry := NULL;
2520                 result_row.authorities := NULL;
2521                 result_row.fields := NULL;
2522                 result_row.value := NULL;
2523                 result_row.sources := NULL;
2524                 result_row.sees := NULL;
2525                 result_row.accurate := NULL;
2526                 result_row.aaccurate := NULL;
2527                 result_row.pivot_point := rec.id;
2528
2529                 IF row_counter >= next_pivot_pos THEN
2530                     RETURN NEXT result_row;
2531                     RETURN;
2532                 END IF;
2533             END IF;
2534
2535             IF count_up_from_zero THEN
2536                 row_number := row_number + 1;
2537             ELSE
2538                 row_number := row_number - 1;
2539             END IF;
2540
2541             -- row_counter is different from row_number.
2542             -- It simply counts up from zero so that we know when
2543             -- we've reached our limit.
2544             row_counter := row_counter + 1;
2545         END IF;
2546     END LOOP;
2547 END;
2548 $f$ LANGUAGE plpgsql ROWS 10;
2549
2550 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON biblio.peer_bib_copy_map;
2551 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON biblio.record_entry;
2552 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.copy;
2553 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.call_number;
2554 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.copy_location;
2555 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON serial.unit;
2556 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON config.copy_status;
2557 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON actor.org_unit;
2558
2559 -- Upgrade the data!
2560 INSERT INTO asset.copy_vis_attr_cache (target_copy, record, vis_attr_vector)
2561     SELECT  cp.id,
2562             cn.record,
2563             asset.calculate_copy_visibility_attribute_set(cp.id)
2564       FROM  asset.copy cp
2565             JOIN asset.call_number cn ON (cp.call_number = cn.id);
2566
2567 -- updating vis cache for biblio.record_entry deferred to end
2568
2569 CREATE TRIGGER z_opac_vis_mat_view_tgr BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2570 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR DELETE ON biblio.peer_bib_copy_map FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2571 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER UPDATE ON asset.call_number FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2572 CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2573 CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2574 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2575 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2576
2577 CREATE OR REPLACE FUNCTION asset.opac_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
2578 DECLARE
2579     ans RECORD;
2580     trans INT;
2581 BEGIN
2582     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
2583
2584     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
2585         RETURN QUERY
2586         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
2587              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
2588              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
2589         SELECT  ans.depth,
2590                 ans.id,
2591                 COUNT( av.id ),
2592                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
2593                 COUNT( av.id ),
2594                 trans
2595           FROM  mask,
2596                 available_statuses,
2597                 org_list,
2598                 asset.copy_vis_attr_cache av
2599                 JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
2600           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
2601           GROUP BY 1,2,6;
2602
2603         IF NOT FOUND THEN
2604             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
2605         END IF;
2606
2607     END LOOP;
2608
2609     RETURN;
2610 END;
2611 $f$ LANGUAGE PLPGSQL;
2612
2613 CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
2614 DECLARE
2615     ans RECORD;
2616     trans INT;
2617 BEGIN
2618     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
2619
2620     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
2621         RETURN QUERY
2622         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
2623              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
2624              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
2625         SELECT  -1,
2626                 ans.id,
2627                 COUNT( av.id ),
2628                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
2629                 COUNT( av.id ),
2630                 trans
2631           FROM  mask,
2632                 org_list,
2633                 asset.copy_vis_attr_cache av
2634                 JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
2635           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
2636           GROUP BY 1,2,6;
2637
2638         IF NOT FOUND THEN
2639             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
2640         END IF;
2641
2642     END LOOP;
2643
2644     RETURN;
2645 END;
2646 $f$ LANGUAGE PLPGSQL;
2647
2648 CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
2649 DECLARE
2650     ans RECORD;
2651     trans INT;
2652 BEGIN
2653     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
2654
2655     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
2656         RETURN QUERY
2657         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
2658              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
2659              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
2660         SELECT  ans.depth,
2661                 ans.id,
2662                 COUNT( av.id ),
2663                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
2664                 COUNT( av.id ),
2665                 trans
2666           FROM  mask,
2667                 org_list,
2668                 available_statuses,
2669                 asset.copy_vis_attr_cache av
2670                 JOIN asset.copy cp ON (cp.id = av.target_copy)
2671                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
2672           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
2673           GROUP BY 1,2,6;
2674
2675         IF NOT FOUND THEN
2676             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
2677         END IF;
2678
2679     END LOOP;
2680
2681     RETURN;
2682 END;
2683 $f$ LANGUAGE PLPGSQL;
2684
2685 CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
2686 DECLARE
2687     ans RECORD;
2688     trans INT;
2689 BEGIN
2690     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
2691
2692     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
2693         RETURN QUERY
2694         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
2695              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
2696              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
2697         SELECT  -1,
2698                 ans.id,
2699                 COUNT( av.id ),
2700                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
2701                 COUNT( av.id ),
2702                 trans
2703           FROM  mask,
2704                 org_list,
2705                 available_statuses,
2706                 asset.copy_vis_attr_cache av
2707                 JOIN asset.copy cp ON (cp.id = av.target_copy)
2708                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
2709           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
2710           GROUP BY 1,2,6;
2711
2712         IF NOT FOUND THEN
2713             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
2714         END IF;
2715
2716     END LOOP;
2717
2718     RETURN;
2719 END;
2720 $f$ LANGUAGE PLPGSQL;
2721
2722 CREATE OR REPLACE FUNCTION unapi.mmr_mra (
2723     obj_id BIGINT,
2724     format TEXT,
2725     ename TEXT,
2726     includes TEXT[],
2727     org TEXT,
2728     depth INT DEFAULT NULL,
2729     slimit HSTORE DEFAULT NULL,
2730     soffset HSTORE DEFAULT NULL,
2731     include_xmlns BOOL DEFAULT TRUE,
2732     pref_lib INT DEFAULT NULL
2733 ) RETURNS XML AS $F$
2734     SELECT  XMLELEMENT(
2735         name attributes,
2736         XMLATTRIBUTES(
2737             CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
2738             'tag:open-ils.org:U2@mmr/' || $1 AS metarecord
2739         ),
2740         (SELECT XMLAGG(foo.y)
2741           FROM (
2742             WITH sourcelist AS (
2743                 WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id FROM actor.org_unit WHERE shortname = $5 LIMIT 1),
2744                      basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
2745                      circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY_AGG(aoud.id)) AS mask
2746                                   FROM aou, LATERAL actor.org_unit_descendants(aou.id, $6) aoud)
2747                 SELECT  source
2748                   FROM  aou, circvm, basevm, metabib.metarecord_source_map mmsm
2749                   WHERE mmsm.metarecord = $1 AND (
2750                     EXISTS (
2751                         SELECT  1
2752                           FROM  circvm, basevm, asset.copy_vis_attr_cache acvac
2753                           WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
2754                                 AND acvac.record = mmsm.source
2755                     )
2756                     OR EXISTS (SELECT 1 FROM evergreen.located_uris(source, aou.id, $10) LIMIT 1)
2757                     OR EXISTS (SELECT 1 FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = mmsm.source)
2758                 )
2759             )
2760             SELECT  cmra.aid,
2761                     XMLELEMENT(
2762                         name field,
2763                         XMLATTRIBUTES(
2764                             cmra.attr AS name,
2765                             cmra.value AS "coded-value",
2766                             cmra.aid AS "cvmid",
2767                             rad.composite,
2768                             rad.multi,
2769                             rad.filter,
2770                             rad.sorter,
2771                             cmra.source_list
2772                         ),
2773                         cmra.value
2774                     )
2775               FROM  (
2776                 SELECT DISTINCT aid, attr, value, STRING_AGG(x.id::TEXT, ',') AS source_list
2777                   FROM (
2778                     SELECT  v.source AS id,
2779                             c.id AS aid,
2780                             c.ctype AS attr,
2781                             c.code AS value
2782                       FROM  metabib.record_attr_vector_list v
2783                             JOIN config.coded_value_map c ON ( c.id = ANY( v.vlist ) )
2784                     ) AS x
2785                     JOIN sourcelist ON (x.id = sourcelist.source)
2786                     GROUP BY 1, 2, 3
2787                 ) AS cmra
2788                 JOIN config.record_attr_definition rad ON (cmra.attr = rad.name)
2789                 UNION ALL
2790             SELECT  umra.aid,
2791                     XMLELEMENT(
2792                         name field,
2793                         XMLATTRIBUTES(
2794                             umra.attr AS name,
2795                             rad.composite,
2796                             rad.multi,
2797                             rad.filter,
2798                             rad.sorter
2799                         ),
2800                         umra.value
2801                     )
2802               FROM  (
2803                 SELECT DISTINCT aid, attr, value
2804                   FROM (
2805                     SELECT  v.source AS id,
2806                             m.id AS aid,
2807                             m.attr AS attr,
2808                             m.value AS value
2809                       FROM  metabib.record_attr_vector_list v
2810                             JOIN metabib.uncontrolled_record_attr_value m ON ( m.id = ANY( v.vlist ) )
2811                     ) AS x
2812                     JOIN sourcelist ON (x.id = sourcelist.source)
2813                 ) AS umra
2814                 JOIN config.record_attr_definition rad ON (umra.attr = rad.name)
2815                 ORDER BY 1
2816
2817             )foo(id,y)
2818         )
2819     )
2820 $F$ LANGUAGE SQL STABLE;
2821
2822 CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
2823     bibid BIGINT[],
2824     ouid INT,
2825     depth INT DEFAULT NULL,
2826     slimit HSTORE DEFAULT NULL,
2827     soffset HSTORE DEFAULT NULL,
2828     pref_lib INT DEFAULT NULL,
2829     includes TEXT[] DEFAULT NULL::TEXT[]
2830 ) RETURNS TABLE(id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT) AS $$
2831     WITH RECURSIVE ou_depth AS (
2832         SELECT COALESCE(
2833             $3,
2834             (
2835                 SELECT depth
2836                 FROM actor.org_unit_type aout
2837                     INNER JOIN actor.org_unit ou ON ou_type = aout.id
2838                 WHERE ou.id = $2
2839             )
2840         ) AS depth
2841     ), descendant_depth AS (
2842         SELECT  ou.id,
2843                 ou.parent_ou,
2844                 out.depth
2845         FROM  actor.org_unit ou
2846                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2847                 JOIN anscestor_depth ad ON (ad.id = ou.id),
2848                 ou_depth
2849         WHERE ad.depth = ou_depth.depth
2850             UNION ALL
2851         SELECT  ou.id,
2852                 ou.parent_ou,
2853                 out.depth
2854         FROM  actor.org_unit ou
2855                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2856                 JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
2857     ), anscestor_depth AS (
2858         SELECT  ou.id,
2859                 ou.parent_ou,
2860                 out.depth
2861         FROM  actor.org_unit ou
2862                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2863         WHERE ou.id = $2
2864             UNION ALL
2865         SELECT  ou.id,
2866                 ou.parent_ou,
2867                 out.depth
2868         FROM  actor.org_unit ou
2869                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2870                 JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
2871     ), descendants as (
2872         SELECT ou.* FROM actor.org_unit ou JOIN descendant_depth USING (id)
2873     )
2874
2875     SELECT ua.id, ua.name, ua.label_sortkey, MIN(ua.rank) AS rank FROM (
2876         SELECT acn.id, owning_lib.name, acn.label_sortkey,
2877             evergreen.rank_cp(acp),
2878             RANK() OVER w
2879         FROM asset.call_number acn
2880             JOIN asset.copy acp ON (acn.id = acp.call_number)
2881             JOIN descendants AS aou ON (acp.circ_lib = aou.id)
2882             JOIN actor.org_unit AS owning_lib ON (acn.owning_lib = owning_lib.id)
2883         WHERE acn.record = ANY ($1)
2884             AND acn.deleted IS FALSE
2885             AND acp.deleted IS FALSE
2886             AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN
2887                 EXISTS (
2888                     WITH basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
2889                          circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY[acp.circ_lib]) AS mask)
2890                     SELECT  1
2891                       FROM  basevm, circvm, asset.copy_vis_attr_cache acvac
2892                       WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
2893                             AND acvac.target_copy = acp.id
2894                             AND acvac.record = acn.record
2895                 ) ELSE TRUE END
2896         GROUP BY acn.id, evergreen.rank_cp(acp), owning_lib.name, acn.label_sortkey, aou.id
2897         WINDOW w AS (
2898             ORDER BY
2899                 COALESCE(
2900                     CASE WHEN aou.id = $2 THEN -20000 END,
2901                     CASE WHEN aou.id = $6 THEN -10000 END,
2902                     (SELECT distance - 5000
2903                         FROM actor.org_unit_descendants_distance($6) as x
2904                         WHERE x.id = aou.id AND $6 IN (
2905                             SELECT q.id FROM actor.org_unit_descendants($2) as q)),
2906                     (SELECT e.distance FROM actor.org_unit_descendants_distance($2) as e WHERE e.id = aou.id),
2907                     1000
2908                 ),
2909                 evergreen.rank_cp(acp)
2910         )
2911     ) AS ua
2912     GROUP BY ua.id, ua.name, ua.label_sortkey
2913     ORDER BY rank, ua.name, ua.label_sortkey
2914     LIMIT ($4 -> 'acn')::INT
2915     OFFSET ($5 -> 'acn')::INT;
2916 $$ LANGUAGE SQL STABLE ROWS 10;
2917
2918
2919 -- Evergreen DB patch XXXX.schema.action-trigger.event_definition.sms_preminder.sql
2920 --
2921 -- New action trigger event definition: 3 Day Courtesy Notice by SMS
2922 --
2923
2924 -- check whether patch can be applied
2925 SELECT evergreen.upgrade_deps_block_check('1058', :eg_version); -- mccanna/csharp/gmcharlt
2926
2927 INSERT INTO action_trigger.event_definition (id, active, owner, name, hook,
2928         validator, reactor, delay, max_delay, delay_field, group_field, template)
2929     VALUES (54, FALSE, 1,
2930         '3 Day Courtesy Notice by SMS',
2931         'checkout.due',
2932         'CircIsOpen', 'SendSMS', '-3 days', '-2 days', 'due_date', 'usr',
2933 $$
2934 [%- USE date -%]
2935 [%- user = target.0.usr -%]
2936 [%- homelib = user.home_ou -%]
2937 [%- sms_number = helpers.get_user_setting(user.id, 'opac.default_sms_notify') -%]
2938 [%- sms_carrier = helpers.get_user_setting(user.id, 'opac.default_sms_carrier') -%]
2939 From: [%- helpers.get_org_setting(homelib.id, 'org.bounced_emails') || homelib.email || params.sender_email || default_sender %]
2940 To: [%- helpers.get_sms_gateway_email(sms_carrier,sms_number) %]
2941 Subject: Library Materials Due Soon
2942
2943 You have items due soon: