Remove redundant function update from 3.0-RC upgrade script
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.12.6-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
1147 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1056', :eg_version); -- miker/gmcharlt
1148
1149 INSERT INTO permission.perm_list (id,code,description) VALUES (592,'CONTAINER_BATCH_UPDATE','Allow batch update via buckets');
1150
1151 INSERT INTO container.user_bucket_type (code,label) SELECT code,label FROM container.copy_bucket_type where code = 'staff_client';
1152
1153 CREATE TABLE action.fieldset_group (
1154     id              SERIAL  PRIMARY KEY,
1155     name            TEXT        NOT NULL,
1156     create_time     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1157     complete_time   TIMESTAMPTZ,
1158     container       INT,        -- Points to a container of some type ...
1159     container_type  TEXT,       -- One of 'biblio_record_entry', 'user', 'call_number', 'copy'
1160     can_rollback    BOOL        DEFAULT TRUE,
1161     rollback_group  INT         REFERENCES action.fieldset_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1162     rollback_time   TIMESTAMPTZ,
1163     creator         INT         NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1164     owning_lib      INT         NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
1165 );
1166
1167 ALTER TABLE action.fieldset ADD COLUMN fieldset_group INT REFERENCES action.fieldset_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1168 ALTER TABLE action.fieldset ADD COLUMN error_msg TEXT;
1169 ALTER TABLE container.biblio_record_entry_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1170 ALTER TABLE container.user_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1171 ALTER TABLE container.call_number_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1172 ALTER TABLE container.copy_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
1173
1174 UPDATE query.stored_query SET id = id + 1000 WHERE id < 1000;
1175 UPDATE query.from_relation SET id = id + 1000 WHERE id < 1000;
1176 UPDATE query.expression SET id = id + 1000 WHERE id < 1000;
1177
1178 SELECT SETVAL('query.stored_query_id_seq', 1, FALSE);
1179 SELECT SETVAL('query.from_relation_id_seq', 1, FALSE);
1180 SELECT SETVAL('query.expression_id_seq', 1, FALSE);
1181
1182 INSERT INTO query.bind_variable (name,type,description,label)
1183     SELECT  'bucket','number','ID of the bucket to pull items from','Bucket ID'
1184       WHERE NOT EXISTS (SELECT 1 FROM query.bind_variable WHERE name = 'bucket');
1185
1186 -- Assumes completely empty 'query' schema
1187 INSERT INTO query.stored_query (type, use_distinct) VALUES ('SELECT', TRUE); -- 1
1188
1189 INSERT INTO query.from_relation (type, table_name, class_name, table_alias) VALUES ('RELATION', 'container.user_bucket_item', 'cubi', 'cubi'); -- 1
1190 UPDATE query.stored_query SET from_clause = 1;
1191
1192 INSERT INTO query.expr_xcol (table_alias, column_name) VALUES ('cubi', 'target_user'); -- 1
1193 INSERT INTO query.select_item (stored_query,seq_no,expression) VALUES (1,1,1);
1194
1195 INSERT INTO query.expr_xcol (table_alias, column_name) VALUES ('cubi', 'bucket'); -- 2
1196 INSERT INTO query.expr_xbind (bind_variable) VALUES ('bucket'); -- 3
1197
1198 INSERT INTO query.expr_xop (left_operand, operator, right_operand) VALUES (2, '=', 3); -- 4
1199 UPDATE query.stored_query SET where_clause = 4;
1200
1201 SELECT SETVAL('query.stored_query_id_seq', 1000, TRUE) FROM query.stored_query;
1202 SELECT SETVAL('query.from_relation_id_seq', 1000, TRUE) FROM query.from_relation;
1203 SELECT SETVAL('query.expression_id_seq', 10000, TRUE) FROM query.expression;
1204
1205 CREATE OR REPLACE FUNCTION action.apply_fieldset(
1206     fieldset_id IN INT,        -- id from action.fieldset
1207     table_name  IN TEXT,       -- table to be updated
1208     pkey_name   IN TEXT,       -- name of primary key column in that table
1209     query       IN TEXT        -- query constructed by qstore (for query-based
1210                                --    fieldsets only; otherwise null
1211 )
1212 RETURNS TEXT AS $$
1213 DECLARE
1214     statement TEXT;
1215     where_clause TEXT;
1216     fs_status TEXT;
1217     fs_pkey_value TEXT;
1218     fs_query TEXT;
1219     sep CHAR;
1220     status_code TEXT;
1221     msg TEXT;
1222     fs_id INT;
1223     fsg_id INT;
1224     update_count INT;
1225     cv RECORD;
1226     fs_obj action.fieldset%ROWTYPE;
1227     fs_group action.fieldset_group%ROWTYPE;
1228     rb_row RECORD;
1229 BEGIN
1230     -- Sanity checks
1231     IF fieldset_id IS NULL THEN
1232         RETURN 'Fieldset ID parameter is NULL';
1233     END IF;
1234     IF table_name IS NULL THEN
1235         RETURN 'Table name parameter is NULL';
1236     END IF;
1237     IF pkey_name IS NULL THEN
1238         RETURN 'Primary key name parameter is NULL';
1239     END IF;
1240
1241     SELECT
1242         status,
1243         quote_literal( pkey_value )
1244     INTO
1245         fs_status,
1246         fs_pkey_value
1247     FROM
1248         action.fieldset
1249     WHERE
1250         id = fieldset_id;
1251
1252     --
1253     -- Build the WHERE clause.  This differs according to whether it's a
1254     -- single-row fieldset or a query-based fieldset.
1255     --
1256     IF query IS NULL        AND fs_pkey_value IS NULL THEN
1257         RETURN 'Incomplete fieldset: neither a primary key nor a query available';
1258     ELSIF query IS NOT NULL AND fs_pkey_value IS NULL THEN
1259         fs_query := rtrim( query, ';' );
1260         where_clause := 'WHERE ' || pkey_name || ' IN ( '
1261                      || fs_query || ' )';
1262     ELSIF query IS NULL     AND fs_pkey_value IS NOT NULL THEN
1263         where_clause := 'WHERE ' || pkey_name || ' = ';
1264         IF pkey_name = 'id' THEN
1265             where_clause := where_clause || fs_pkey_value;
1266         ELSIF pkey_name = 'code' THEN
1267             where_clause := where_clause || quote_literal(fs_pkey_value);
1268         ELSE
1269             RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
1270         END IF;
1271     ELSE  -- both are not null
1272         RETURN 'Ambiguous fieldset: both a primary key and a query provided';
1273     END IF;
1274
1275     IF fs_status IS NULL THEN
1276         RETURN 'No fieldset found for id = ' || fieldset_id;
1277     ELSIF fs_status = 'APPLIED' THEN
1278         RETURN 'Fieldset ' || fieldset_id || ' has already been applied';
1279     END IF;
1280
1281     SELECT * INTO fs_obj FROM action.fieldset WHERE id = fieldset_id;
1282     SELECT * INTO fs_group FROM action.fieldset_group WHERE id = fs_obj.fieldset_group;
1283
1284     IF fs_group.can_rollback THEN
1285         -- This is part of a non-rollback group.  We need to record the current values for future rollback.
1286
1287         INSERT INTO action.fieldset_group (can_rollback, name, creator, owning_lib, container, container_type)
1288             VALUES (FALSE, 'ROLLBACK: '|| fs_group.name, fs_group.creator, fs_group.owning_lib, fs_group.container, fs_group.container_type);
1289
1290         fsg_id := CURRVAL('action.fieldset_group_id_seq');
1291
1292         FOR rb_row IN EXECUTE 'SELECT * FROM ' || table_name || ' ' || where_clause LOOP
1293             IF pkey_name = 'id' THEN
1294                 fs_pkey_value := rb_row.id;
1295             ELSIF pkey_name = 'code' THEN
1296                 fs_pkey_value := rb_row.code;
1297             ELSE
1298                 RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
1299             END IF;
1300             INSERT INTO action.fieldset (fieldset_group,owner,owning_lib,status,classname,name,pkey_value)
1301                 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);
1302
1303             fs_id := CURRVAL('action.fieldset_id_seq');
1304             sep := '';
1305             FOR cv IN
1306                 SELECT  DISTINCT col
1307                 FROM    action.fieldset_col_val
1308                 WHERE   fieldset = fieldset_id
1309             LOOP
1310                 EXECUTE 'INSERT INTO action.fieldset_col_val (fieldset, col, val) ' || 
1311                     'SELECT '|| fs_id || ', '||quote_literal(cv.col)||', '||cv.col||' FROM '||table_name||' WHERE '||pkey_name||' = '||fs_pkey_value;
1312             END LOOP;
1313         END LOOP;
1314     END IF;
1315
1316     statement := 'UPDATE ' || table_name || ' SET';
1317
1318     sep := '';
1319     FOR cv IN
1320         SELECT  col,
1321                 val
1322         FROM    action.fieldset_col_val
1323         WHERE   fieldset = fieldset_id
1324     LOOP
1325         statement := statement || sep || ' ' || cv.col
1326                      || ' = ' || coalesce( quote_literal( cv.val ), 'NULL' );
1327         sep := ',';
1328     END LOOP;
1329
1330     IF sep = '' THEN
1331         RETURN 'Fieldset ' || fieldset_id || ' has no column values defined';
1332     END IF;
1333     statement := statement || ' ' || where_clause;
1334
1335     --
1336     -- Execute the update
1337     --
1338     BEGIN
1339         EXECUTE statement;
1340         GET DIAGNOSTICS update_count = ROW_COUNT;
1341
1342         IF update_count = 0 THEN
1343             RAISE data_exception;
1344         END IF;
1345
1346         IF fsg_id IS NOT NULL THEN
1347             UPDATE action.fieldset_group SET rollback_group = fsg_id WHERE id = fs_group.id;
1348         END IF;
1349
1350         IF fs_group.id IS NOT NULL THEN
1351             UPDATE action.fieldset_group SET complete_time = now() WHERE id = fs_group.id;
1352         END IF;
1353
1354         UPDATE action.fieldset SET status = 'APPLIED', applied_time = now() WHERE id = fieldset_id;
1355
1356     EXCEPTION WHEN data_exception THEN
1357         msg := 'No eligible rows found for fieldset ' || fieldset_id;
1358         UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
1359         RETURN msg;
1360
1361     END;
1362
1363     RETURN msg;
1364
1365 EXCEPTION WHEN OTHERS THEN
1366     msg := 'Unable to apply fieldset ' || fieldset_id || ': ' || sqlerrm;
1367     UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
1368     RETURN msg;
1369
1370 END;
1371 $$ LANGUAGE plpgsql;
1372
1373
1374
1375 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1057', :eg_version); -- miker/gmcharlt/kmlussier
1376
1377 -- Thist change drops a needless join and saves 10-15% in time cost
1378 CREATE OR REPLACE FUNCTION search.facets_for_record_set(ignore_facet_classes text[], hits bigint[]) RETURNS TABLE(id integer, value text, count bigint)
1379 AS $f$
1380     SELECT id, value, count
1381       FROM (
1382         SELECT  mfae.field AS id,
1383                 mfae.value,
1384                 COUNT(DISTINCT mfae.source),
1385                 row_number() OVER (
1386                     PARTITION BY mfae.field ORDER BY COUNT(DISTINCT mfae.source) DESC
1387                 ) AS rownum
1388           FROM  metabib.facet_entry mfae
1389                 JOIN config.metabib_field cmf ON (cmf.id = mfae.field)
1390           WHERE mfae.source = ANY ($2)
1391                 AND cmf.facet_field
1392                 AND cmf.field_class NOT IN (SELECT * FROM unnest($1))
1393           GROUP by 1, 2
1394       ) all_facets
1395       WHERE rownum <= (
1396         SELECT COALESCE(
1397             (SELECT value::INT FROM config.global_flag WHERE name = 'search.max_facets_per_field' AND enabled),
1398             1000
1399         )
1400       );
1401 $f$ LANGUAGE SQL;
1402
1403 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$
1404 DECLARE
1405     layout          unapi.bre_output_layout%ROWTYPE;
1406     transform       config.xml_transform%ROWTYPE;
1407     item_format     TEXT;
1408     tmp_xml         TEXT;
1409     xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
1410     ouid            INT;
1411     element_list    TEXT[];
1412 BEGIN
1413
1414     IF org = '-' OR org IS NULL THEN
1415         SELECT shortname INTO org FROM evergreen.org_top();
1416     END IF;
1417
1418     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
1419     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
1420
1421     IF layout.name IS NULL THEN
1422         RETURN NULL::XML;
1423     END IF;
1424
1425     SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
1426     xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
1427
1428     -- Gather the bib xml
1429     SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
1430
1431     IF layout.title_element IS NOT NULL THEN
1432         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
1433     END IF;
1434
1435     IF layout.description_element IS NOT NULL THEN
1436         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
1437     END IF;
1438
1439     IF layout.creator_element IS NOT NULL THEN
1440         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
1441     END IF;
1442
1443     IF layout.update_ts_element IS NOT NULL THEN
1444         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;
1445     END IF;
1446
1447     IF unapi_url IS NOT NULL THEN
1448         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;
1449     END IF;
1450
1451     IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
1452
1453     element_list := regexp_split_to_array(layout.feed_top,E'\\.');
1454     FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
1455         EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
1456     END LOOP;
1457
1458     RETURN tmp_xml::XML;
1459 END;
1460 $F$ LANGUAGE PLPGSQL STABLE;
1461
1462 CREATE TABLE asset.copy_vis_attr_cache (
1463     id              BIGSERIAL   PRIMARY KEY,
1464     record          BIGINT      NOT NULL, -- No FKEYs, managed by user triggers.
1465     target_copy     BIGINT      NOT NULL,
1466     vis_attr_vector INT[]
1467 );
1468 CREATE INDEX copy_vis_attr_cache_record_idx ON asset.copy_vis_attr_cache (record);
1469 CREATE INDEX copy_vis_attr_cache_copy_idx ON asset.copy_vis_attr_cache (target_copy);
1470
1471 ALTER TABLE biblio.record_entry ADD COLUMN vis_attr_vector INT[];
1472
1473 CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute ( value INT, attr TEXT ) RETURNS INT AS $f$
1474 SELECT  ((CASE $2
1475
1476             WHEN 'luri_org'         THEN 0 -- "b" attr
1477             WHEN 'bib_source'       THEN 1 -- "b" attr
1478
1479             WHEN 'copy_flags'       THEN 0 -- "c" attr
1480             WHEN 'owning_lib'       THEN 1 -- "c" attr
1481             WHEN 'circ_lib'         THEN 2 -- "c" attr
1482             WHEN 'status'           THEN 3 -- "c" attr
1483             WHEN 'location'         THEN 4 -- "c" attr
1484             WHEN 'location_group'   THEN 5 -- "c" attr
1485
1486         END) << 28 ) | $1;
1487
1488 /* copy_flags bit positions, LSB-first:
1489
1490  0: asset.copy.opac_visible
1491
1492
1493    When adding flags, you must update asset.all_visible_flags()
1494
1495    Because bib and copy values are stored separately, we can reuse
1496    shifts, saving us some space. We could probably take back a bit
1497    too, but I'm not sure its worth squeezing that last one out. We'd
1498    be left with just 2 slots for copy attrs, rather than 10.
1499 */
1500
1501 $f$ LANGUAGE SQL IMMUTABLE;
1502
1503 CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_list ( attr TEXT, value INT[] ) RETURNS INT[] AS $f$
1504     SELECT ARRAY_AGG(search.calculate_visibility_attribute(x, $1)) FROM UNNEST($2) AS X;
1505 $f$ LANGUAGE SQL IMMUTABLE;
1506
1507 CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_test ( attr TEXT, value INT[], negate BOOL DEFAULT FALSE ) RETURNS TEXT AS $f$
1508     SELECT  CASE WHEN $3 THEN '!' ELSE '' END || '(' || ARRAY_TO_STRING(search.calculate_visibility_attribute_list($1,$2),'|') || ')';
1509 $f$ LANGUAGE SQL IMMUTABLE;
1510
1511 CREATE OR REPLACE FUNCTION asset.calculate_copy_visibility_attribute_set ( copy_id BIGINT ) RETURNS INT[] AS $f$
1512 DECLARE
1513     copy_row    asset.copy%ROWTYPE;
1514     lgroup_map  asset.copy_location_group_map%ROWTYPE;
1515     attr_set    INT[];
1516 BEGIN
1517     SELECT * INTO copy_row FROM asset.copy WHERE id = copy_id;
1518
1519     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.opac_visible::INT, 'copy_flags');
1520     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.circ_lib, 'circ_lib');
1521     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.status, 'status');
1522     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.location, 'location');
1523
1524     SELECT  ARRAY_APPEND(
1525                 attr_set,
1526                 search.calculate_visibility_attribute(owning_lib, 'owning_lib')
1527             ) INTO attr_set
1528       FROM  asset.call_number
1529       WHERE id = copy_row.call_number;
1530
1531     FOR lgroup_map IN SELECT * FROM asset.copy_location_group_map WHERE location = copy_row.location LOOP
1532         attr_set := attr_set || search.calculate_visibility_attribute(lgroup_map.lgroup, 'location_group');
1533     END LOOP;
1534
1535     RETURN attr_set;
1536 END;
1537 $f$ LANGUAGE PLPGSQL;
1538
1539 CREATE OR REPLACE FUNCTION biblio.calculate_bib_visibility_attribute_set ( bib_id BIGINT ) RETURNS INT[] AS $f$
1540 DECLARE
1541     bib_row     biblio.record_entry%ROWTYPE;
1542     cn_row      asset.call_number%ROWTYPE;
1543     attr_set    INT[];
1544 BEGIN
1545     SELECT * INTO bib_row FROM biblio.record_entry WHERE id = bib_id;
1546
1547     IF bib_row.source IS NOT NULL THEN
1548         attr_set := attr_set || search.calculate_visibility_attribute(bib_row.source, 'bib_source');
1549     END IF;
1550
1551     FOR cn_row IN
1552         SELECT  cn.*
1553           FROM  asset.call_number cn
1554                 JOIN asset.uri_call_number_map m ON (cn.id = m.call_number)
1555                 JOIN asset.uri u ON (u.id = m.uri)
1556           WHERE cn.record = bib_id
1557                 AND cn.label = '##URI##'
1558                 AND u.active
1559     LOOP
1560         attr_set := attr_set || search.calculate_visibility_attribute(cn_row.owning_lib, 'luri_org');
1561     END LOOP;
1562
1563     RETURN attr_set;
1564 END;
1565 $f$ LANGUAGE PLPGSQL;
1566
1567
1568 SELECT evergreen.upgrade_deps_block_check('1076', :eg_version); -- miker/gmcharlt
1569
1570 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
1571 DECLARE
1572     ocn     asset.call_number%ROWTYPE;
1573     ncn     asset.call_number%ROWTYPE;
1574     cid     BIGINT;
1575 BEGIN
1576
1577     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN -- Only needs ON INSERT OR DELETE, so handle separately
1578         IF TG_OP = 'INSERT' THEN
1579             INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
1580                 NEW.peer_record,
1581                 NEW.target_copy,
1582                 asset.calculate_copy_visibility_attribute_set(NEW.target_copy)
1583             );
1584
1585             RETURN NEW;
1586         ELSIF TG_OP = 'DELETE' THEN
1587             DELETE FROM asset.copy_vis_attr_cache
1588               WHERE record = NEW.peer_record AND target_copy = NEW.target_copy;
1589
1590             RETURN OLD;
1591         END IF;
1592     END IF;
1593
1594     IF TG_OP = 'INSERT' THEN -- Handles ON INSERT. ON UPDATE is below.
1595         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
1596             SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
1597             INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
1598                 ncn.record,
1599                 NEW.id,
1600                 asset.calculate_copy_visibility_attribute_set(NEW.id)
1601             );
1602         ELSIF TG_TABLE_NAME = 'record_entry' THEN
1603             NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
1604         END IF;
1605
1606         RETURN NEW;
1607     END IF;
1608
1609     -- handle items first, since with circulation activity
1610     -- their statuses change frequently
1611     IF TG_TABLE_NAME IN ('copy', 'unit') THEN -- This handles ON UPDATE OR DELETE. ON INSERT above
1612
1613         IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
1614             DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
1615             RETURN OLD;
1616         END IF;
1617
1618         SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
1619
1620         IF OLD.deleted <> NEW.deleted THEN
1621             IF NEW.deleted THEN
1622                 DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
1623             ELSE
1624                 INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
1625                     ncn.record,
1626                     NEW.id,
1627                     asset.calculate_copy_visibility_attribute_set(NEW.id)
1628                 );
1629             END IF;
1630
1631             RETURN NEW;
1632         ELSIF OLD.call_number  <> NEW.call_number THEN
1633             SELECT * INTO ocn FROM asset.call_number cn WHERE id = OLD.call_number;
1634
1635             IF ncn.record <> ocn.record THEN
1636                 UPDATE  biblio.record_entry
1637                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(ncn.record)
1638                   WHERE id = ocn.record;
1639
1640                 -- We have to use a record-specific WHERE clause
1641                 -- to avoid modifying the entries for peer-bib copies.
1642                 UPDATE  asset.copy_vis_attr_cache
1643                   SET   target_copy = NEW.id,
1644                         record = ncn.record
1645                   WHERE target_copy = OLD.id
1646                         AND record = ocn.record;
1647             END IF;
1648         END IF;
1649
1650         IF OLD.location     <> NEW.location OR
1651            OLD.status       <> NEW.status OR
1652            OLD.opac_visible <> NEW.opac_visible OR
1653            OLD.circ_lib     <> NEW.circ_lib
1654         THEN
1655             -- Any of these could change visibility, but
1656             -- we'll save some queries and not try to calculate
1657             -- the change directly.  We want to update peer-bib
1658             -- entries in this case, unlike above.
1659             UPDATE  asset.copy_vis_attr_cache
1660               SET   target_copy = NEW.id,
1661                     vis_attr_vector = asset.calculate_copy_visibility_attribute_set(NEW.id)
1662               WHERE target_copy = OLD.id;
1663
1664         END IF;
1665
1666     ELSIF TG_TABLE_NAME = 'call_number' THEN -- Only ON UPDATE. Copy handler will deal with ON INSERT OR DELETE.
1667
1668         IF OLD.record <> NEW.record THEN
1669             IF NEW.label = '##URI##' THEN
1670                 UPDATE  biblio.record_entry
1671                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
1672                   WHERE id = OLD.record;
1673
1674                 UPDATE  biblio.record_entry
1675                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(NEW.record)
1676                   WHERE id = NEW.record;
1677             END IF;
1678
1679             UPDATE  asset.copy_vis_attr_cache
1680               SET   record = NEW.record,
1681                     vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
1682               WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
1683                     AND record = OLD.record;
1684
1685         ELSIF OLD.owning_lib <> NEW.owning_lib THEN
1686             UPDATE  asset.copy_vis_attr_cache
1687               SET   vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
1688               WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
1689                     AND record = NEW.record;
1690
1691             IF NEW.label = '##URI##' THEN
1692                 UPDATE  biblio.record_entry
1693                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
1694                   WHERE id = OLD.record;
1695             END IF;
1696         END IF;
1697
1698     ELSIF TG_TABLE_NAME = 'record_entry' THEN -- Only handles ON UPDATE OR DELETE
1699
1700         IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
1701             DELETE FROM asset.copy_vis_attr_cache WHERE record = OLD.id;
1702             RETURN OLD;
1703         ELSIF OLD.source <> NEW.source THEN
1704             NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
1705         END IF;
1706
1707     END IF;
1708
1709     RETURN NEW;
1710 END;
1711 $func$ LANGUAGE PLPGSQL;
1712
1713
1714 -- Helper functions for use in constructing searches --
1715
1716 CREATE OR REPLACE FUNCTION asset.all_visible_flags () RETURNS TEXT AS $f$
1717     SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(1 << x, 'copy_flags')),'&') || ')'
1718       FROM  GENERATE_SERIES(0,0) AS x; -- increment as new flags are added.
1719 $f$ LANGUAGE SQL STABLE;
1720
1721 CREATE OR REPLACE FUNCTION asset.visible_orgs (otype TEXT) RETURNS TEXT AS $f$
1722     SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
1723       FROM  actor.org_unit
1724       WHERE opac_visible;
1725 $f$ LANGUAGE SQL STABLE;
1726
1727 CREATE OR REPLACE FUNCTION asset.invisible_orgs (otype TEXT) RETURNS TEXT AS $f$
1728     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
1729       FROM  actor.org_unit
1730       WHERE NOT opac_visible;
1731 $f$ LANGUAGE SQL STABLE;
1732
1733 -- Bib-oriented defaults for search
1734 CREATE OR REPLACE FUNCTION asset.bib_source_default () RETURNS TEXT AS $f$
1735     SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'bib_source')),'|') || ')'
1736       FROM  config.bib_source
1737       WHERE transcendant;
1738 $f$ LANGUAGE SQL IMMUTABLE;
1739
1740 CREATE OR REPLACE FUNCTION asset.luri_org_default () RETURNS TEXT AS $f$
1741     SELECT  * FROM asset.invisible_orgs('luri_org');
1742 $f$ LANGUAGE SQL STABLE;
1743
1744 -- Copy-oriented defaults for search
1745 CREATE OR REPLACE FUNCTION asset.location_group_default () RETURNS TEXT AS $f$
1746     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location_group')),'|') || ')'
1747       FROM  asset.copy_location_group
1748       WHERE NOT opac_visible;
1749 $f$ LANGUAGE SQL STABLE;
1750
1751 CREATE OR REPLACE FUNCTION asset.location_default () RETURNS TEXT AS $f$
1752     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location')),'|') || ')'
1753       FROM  asset.copy_location
1754       WHERE NOT opac_visible;
1755 $f$ LANGUAGE SQL STABLE;
1756
1757 CREATE OR REPLACE FUNCTION asset.status_default () RETURNS TEXT AS $f$
1758     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'status')),'|') || ')'
1759       FROM  config.copy_status
1760       WHERE NOT opac_visible;
1761 $f$ LANGUAGE SQL STABLE;
1762
1763 CREATE OR REPLACE FUNCTION asset.owning_lib_default () RETURNS TEXT AS $f$
1764     SELECT  * FROM asset.invisible_orgs('owning_lib');
1765 $f$ LANGUAGE SQL STABLE;
1766
1767 CREATE OR REPLACE FUNCTION asset.circ_lib_default () RETURNS TEXT AS $f$
1768     SELECT  * FROM asset.invisible_orgs('circ_lib');
1769 $f$ LANGUAGE SQL STABLE;
1770
1771 CREATE OR REPLACE FUNCTION asset.patron_default_visibility_mask () RETURNS TABLE (b_attrs TEXT, c_attrs TEXT)  AS $f$
1772 DECLARE
1773     copy_flags      TEXT; -- "c" attr
1774
1775     owning_lib      TEXT; -- "c" attr
1776     circ_lib        TEXT; -- "c" attr
1777     status          TEXT; -- "c" attr
1778     location        TEXT; -- "c" attr
1779     location_group  TEXT; -- "c" attr
1780
1781     luri_org        TEXT; -- "b" attr
1782     bib_sources     TEXT; -- "b" attr
1783 BEGIN
1784     copy_flags      := asset.all_visible_flags(); -- Will always have at least one
1785
1786     owning_lib      := NULLIF(asset.owning_lib_default(),'!()');
1787     
1788     circ_lib        := NULLIF(asset.circ_lib_default(),'!()');
1789     status          := NULLIF(asset.status_default(),'!()');
1790     location        := NULLIF(asset.location_default(),'!()');
1791     location_group  := NULLIF(asset.location_group_default(),'!()');
1792
1793     luri_org        := NULLIF(asset.luri_org_default(),'!()');
1794     bib_sources     := NULLIF(asset.bib_source_default(),'()');
1795
1796     RETURN QUERY SELECT
1797         '('||ARRAY_TO_STRING(
1798             ARRAY[luri_org,bib_sources],
1799             '|'
1800         )||')',
1801         '('||ARRAY_TO_STRING(
1802             ARRAY[copy_flags,owning_lib,circ_lib,status,location,location_group]::TEXT[],
1803             '&'
1804         )||')';
1805 END;
1806 $f$ LANGUAGE PLPGSQL STABLE ROWS 1;
1807
1808 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)
1809  RETURNS TABLE(value text, field integer, buoyant_and_class_match boolean, field_match boolean, field_weight integer, rank real, buoyant boolean, match text)
1810 AS $f$
1811 DECLARE
1812     prepared_query_texts    TEXT[];
1813     query                   TSQUERY;
1814     plain_query             TSQUERY;
1815     opac_visibility_join    TEXT;
1816     search_class_join       TEXT;
1817     r_fields                RECORD;
1818 BEGIN
1819     prepared_query_texts := metabib.autosuggest_prepare_tsquery(raw_query_text);
1820
1821     query := TO_TSQUERY('keyword', prepared_query_texts[1]);
1822     plain_query := TO_TSQUERY('keyword', prepared_query_texts[2]);
1823
1824     visibility_org := NULLIF(visibility_org,-1);
1825     IF visibility_org IS NOT NULL THEN
1826         PERFORM FROM actor.org_unit WHERE id = visibility_org AND parent_ou IS NULL;
1827         IF FOUND THEN
1828             opac_visibility_join := '';
1829         ELSE
1830             opac_visibility_join := '
1831     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = x.source)
1832     JOIN vm ON (acvac.vis_attr_vector @@
1833             (vm.c_attrs || $$&$$ ||
1834                 search.calculate_visibility_attribute_test(
1835                     $$circ_lib$$,
1836                     (SELECT ARRAY_AGG(id) FROM actor.org_unit_descendants($4))
1837                 )
1838             )::query_int
1839          )
1840 ';
1841         END IF;
1842     ELSE
1843         opac_visibility_join := '';
1844     END IF;
1845
1846     -- The following determines whether we only provide suggestsons matching
1847     -- the user's selected search_class, or whether we show other suggestions
1848     -- too. The reason for MIN() is that for search_classes like
1849     -- 'title|proper|uniform' you would otherwise get multiple rows.  The
1850     -- implication is that if title as a class doesn't have restrict,
1851     -- nor does the proper field, but the uniform field does, you're going
1852     -- to get 'false' for your overall evaluation of 'should we restrict?'
1853     -- To invert that, change from MIN() to MAX().
1854
1855     SELECT
1856         INTO r_fields
1857             MIN(cmc.restrict::INT) AS restrict_class,
1858             MIN(cmf.restrict::INT) AS restrict_field
1859         FROM metabib.search_class_to_registered_components(search_class)
1860             AS _registered (field_class TEXT, field INT)
1861         JOIN
1862             config.metabib_class cmc ON (cmc.name = _registered.field_class)
1863         LEFT JOIN
1864             config.metabib_field cmf ON (cmf.id = _registered.field);
1865
1866     -- evaluate 'should we restrict?'
1867     IF r_fields.restrict_field::BOOL OR r_fields.restrict_class::BOOL THEN
1868         search_class_join := '
1869     JOIN
1870         metabib.search_class_to_registered_components($2)
1871         AS _registered (field_class TEXT, field INT) ON (
1872             (_registered.field IS NULL AND
1873                 _registered.field_class = cmf.field_class) OR
1874             (_registered.field = cmf.id)
1875         )
1876     ';
1877     ELSE
1878         search_class_join := '
1879     LEFT JOIN
1880         metabib.search_class_to_registered_components($2)
1881         AS _registered (field_class TEXT, field INT) ON (
1882             _registered.field_class = cmc.name
1883         )
1884     ';
1885     END IF;
1886
1887     RETURN QUERY EXECUTE '
1888 WITH vm AS ( SELECT * FROM asset.patron_default_visibility_mask() ),
1889      mbe AS (SELECT * FROM metabib.browse_entry WHERE index_vector @@ $1 LIMIT 10000)
1890 SELECT  DISTINCT
1891         x.value,
1892         x.id,
1893         x.push,
1894         x.restrict,
1895         x.weight,
1896         x.ts_rank_cd,
1897         x.buoyant,
1898         TS_HEADLINE(value, $7, $3)
1899   FROM  (SELECT DISTINCT
1900                 mbe.value,
1901                 cmf.id,
1902                 cmc.buoyant AND _registered.field_class IS NOT NULL AS push,
1903                 _registered.field = cmf.id AS restrict,
1904                 cmf.weight,
1905                 TS_RANK_CD(mbe.index_vector, $1, $6),
1906                 cmc.buoyant,
1907                 mbedm.source
1908           FROM  metabib.browse_entry_def_map mbedm
1909                 JOIN mbe ON (mbe.id = mbedm.entry)
1910                 JOIN config.metabib_field cmf ON (cmf.id = mbedm.def)
1911                 JOIN config.metabib_class cmc ON (cmf.field_class = cmc.name)
1912                 '  || search_class_join || '
1913           ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
1914           LIMIT 1000) AS x
1915         ' || opac_visibility_join || '
1916   ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
1917   LIMIT $5
1918 '   -- sic, repeat the order by clause in the outer select too
1919     USING
1920         query, search_class, headline_opts,
1921         visibility_org, query_limit, normalization, plain_query
1922         ;
1923
1924     -- sort order:
1925     --  buoyant AND chosen class = match class
1926     --  chosen field = match field
1927     --  field weight
1928     --  rank
1929     --  buoyancy
1930     --  value itself
1931
1932 END;
1933 $f$ LANGUAGE plpgsql ROWS 10;
1934
1935 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)
1936  RETURNS SETOF metabib.flat_browse_entry_appearance
1937 AS $f$
1938 DECLARE
1939     core_query              TEXT;
1940     back_query              TEXT;
1941     forward_query           TEXT;
1942     pivot_sort_value        TEXT;
1943     pivot_sort_fallback     TEXT;
1944     context_locations       INT[];
1945     browse_superpage_size   INT;
1946     results_skipped         INT := 0;
1947     back_limit              INT;
1948     back_to_pivot           INT;
1949     forward_limit           INT;
1950     forward_to_pivot        INT;
1951 BEGIN
1952     -- First, find the pivot if we were given a browse term but not a pivot.
1953     IF pivot_id IS NULL THEN
1954         pivot_id := metabib.browse_pivot(search_field, browse_term);
1955     END IF;
1956
1957     SELECT INTO pivot_sort_value, pivot_sort_fallback
1958         sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
1959
1960     -- Bail if we couldn't find a pivot.
1961     IF pivot_sort_value IS NULL THEN
1962         RETURN;
1963     END IF;
1964
1965     -- Transform the context_loc_group argument (if any) (logc at the
1966     -- TPAC layer) into a form we'll be able to use.
1967     IF context_loc_group IS NOT NULL THEN
1968         SELECT INTO context_locations ARRAY_AGG(location)
1969             FROM asset.copy_location_group_map
1970             WHERE lgroup = context_loc_group;
1971     END IF;
1972
1973     -- Get the configured size of browse superpages.
1974     SELECT INTO browse_superpage_size COALESCE(value::INT,100)     -- NULL ok
1975         FROM config.global_flag
1976         WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
1977
1978     -- First we're going to search backward from the pivot, then we're going
1979     -- to search forward.  In each direction, we need two limits.  At the
1980     -- lesser of the two limits, we delineate the edge of the result set
1981     -- we're going to return.  At the greater of the two limits, we find the
1982     -- pivot value that would represent an offset from the current pivot
1983     -- at a distance of one "page" in either direction, where a "page" is a
1984     -- result set of the size specified in the "result_limit" argument.
1985     --
1986     -- The two limits in each direction make four derived values in total,
1987     -- and we calculate them now.
1988     back_limit := CEIL(result_limit::FLOAT / 2);
1989     back_to_pivot := result_limit;
1990     forward_limit := result_limit / 2;
1991     forward_to_pivot := result_limit - 1;
1992
1993     -- This is the meat of the SQL query that finds browse entries.  We'll
1994     -- pass this to a function which uses it with a cursor, so that individual
1995     -- rows may be fetched in a loop until some condition is satisfied, without
1996     -- waiting for a result set of fixed size to be collected all at once.
1997     core_query := '
1998 SELECT  mbe.id,
1999         mbe.value,
2000         mbe.sort_value
2001   FROM  metabib.browse_entry mbe
2002   WHERE (
2003             EXISTS ( -- are there any bibs using this mbe via the requested fields?
2004                 SELECT  1
2005                   FROM  metabib.browse_entry_def_map mbedm
2006                   WHERE mbedm.entry = mbe.id AND mbedm.def = ANY(' || quote_literal(search_field) || ')
2007             ) OR EXISTS ( -- are there any authorities using this mbe via the requested fields?
2008                 SELECT  1
2009                   FROM  metabib.browse_entry_simple_heading_map mbeshm
2010                         JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2011                         JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2012                             ash.atag = map.authority_field
2013                             AND map.metabib_field = ANY(' || quote_literal(search_field) || ')
2014                         )
2015                   WHERE mbeshm.entry = mbe.id
2016             )
2017         ) AND ';
2018
2019     -- This is the variant of the query for browsing backward.
2020     back_query := core_query ||
2021         ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
2022     ' ORDER BY mbe.sort_value DESC, mbe.value DESC LIMIT 1000';
2023
2024     -- This variant browses forward.
2025     forward_query := core_query ||
2026         ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
2027     ' ORDER BY mbe.sort_value, mbe.value LIMIT 1000';
2028
2029     -- We now call the function which applies a cursor to the provided
2030     -- queries, stopping at the appropriate limits and also giving us
2031     -- the next page's pivot.
2032     RETURN QUERY
2033         SELECT * FROM metabib.staged_browse(
2034             back_query, search_field, context_org, context_locations,
2035             staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
2036         ) UNION
2037         SELECT * FROM metabib.staged_browse(
2038             forward_query, search_field, context_org, context_locations,
2039             staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
2040         ) ORDER BY row_number DESC;
2041
2042 END;
2043 $f$ LANGUAGE plpgsql ROWS 10;
2044
2045 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)
2046  RETURNS SETOF metabib.flat_browse_entry_appearance
2047 AS $f$
2048 DECLARE
2049     curs                    REFCURSOR;
2050     rec                     RECORD;
2051     qpfts_query             TEXT;
2052     aqpfts_query            TEXT;
2053     afields                 INT[];
2054     bfields                 INT[];
2055     result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
2056     results_skipped         INT := 0;
2057     row_counter             INT := 0;
2058     row_number              INT;
2059     slice_start             INT;
2060     slice_end               INT;
2061     full_end                INT;
2062     all_records             BIGINT[];
2063     all_brecords             BIGINT[];
2064     all_arecords            BIGINT[];
2065     superpage_of_records    BIGINT[];
2066     superpage_size          INT;
2067     c_tests                 TEXT := '';
2068     b_tests                 TEXT := '';
2069     c_orgs                  INT[];
2070 BEGIN
2071     IF count_up_from_zero THEN
2072         row_number := 0;
2073     ELSE
2074         row_number := -1;
2075     END IF;
2076
2077     IF NOT staff THEN
2078         SELECT x.c_attrs, x.b_attrs INTO c_tests, b_tests FROM asset.patron_default_visibility_mask() x;
2079     END IF;
2080
2081     IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
2082     IF b_tests <> '' THEN b_tests := b_tests || '&'; END IF;
2083
2084     SELECT ARRAY_AGG(id) INTO c_orgs FROM actor.org_unit_descendants(context_org);
2085     
2086     c_tests := c_tests || search.calculate_visibility_attribute_test('circ_lib',c_orgs)
2087                || '&' || search.calculate_visibility_attribute_test('owning_lib',c_orgs);
2088     
2089     PERFORM 1 FROM config.internal_flag WHERE enabled AND name = 'opac.located_uri.act_as_copy';
2090     IF FOUND THEN
2091         b_tests := b_tests || search.calculate_visibility_attribute_test(
2092             'luri_org',
2093             (SELECT ARRAY_AGG(id) FROM actor.org_unit_full_path(context_org) x)
2094         );
2095     ELSE
2096         b_tests := b_tests || search.calculate_visibility_attribute_test(
2097             'luri_org',
2098             (SELECT ARRAY_AGG(id) FROM actor.org_unit_ancestors(context_org) x)
2099         );
2100     END IF;
2101
2102     IF context_locations THEN
2103         IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
2104         c_tests := c_tests || search.calculate_visibility_attribute_test('location',context_locations);
2105     END IF;
2106
2107     OPEN curs NO SCROLL FOR EXECUTE query;
2108
2109     LOOP
2110         FETCH curs INTO rec;
2111         IF NOT FOUND THEN
2112             IF result_row.pivot_point IS NOT NULL THEN
2113                 RETURN NEXT result_row;
2114             END IF;
2115             RETURN;
2116         END IF;
2117
2118         -- Gather aggregate data based on the MBE row we're looking at now, authority axis
2119         SELECT INTO all_arecords, result_row.sees, afields
2120                 ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
2121                 STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
2122                 ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
2123
2124           FROM  metabib.browse_entry_simple_heading_map mbeshm
2125                 JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
2126                 JOIN authority.authority_linking aal ON ( ash.record = aal.source )
2127                 JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
2128                 JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
2129                     ash.atag = map.authority_field
2130                     AND map.metabib_field = ANY(fields)
2131                 )
2132           WHERE mbeshm.entry = rec.id;
2133
2134         -- Gather aggregate data based on the MBE row we're looking at now, bib axis
2135         SELECT INTO all_brecords, result_row.authorities, bfields
2136                 ARRAY_AGG(DISTINCT source),
2137                 STRING_AGG(DISTINCT authority::TEXT, $$,$$),
2138                 ARRAY_AGG(DISTINCT def)
2139           FROM  metabib.browse_entry_def_map
2140           WHERE entry = rec.id
2141                 AND def = ANY(fields);
2142
2143         SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
2144
2145         result_row.sources := 0;
2146         result_row.asources := 0;
2147
2148         -- Bib-linked vis checking
2149         IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
2150
2151             SELECT  INTO result_row.sources COUNT(DISTINCT b.id)
2152               FROM  biblio.record_entry b
2153                     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
2154               WHERE b.id = ANY(all_brecords[1:browse_superpage_size])
2155                     AND (
2156                         acvac.vis_attr_vector @@ c_tests::query_int
2157                         OR b.vis_attr_vector @@ b_tests::query_int
2158                     );
2159
2160             result_row.accurate := TRUE;
2161
2162         END IF;
2163
2164         -- Authority-linked vis checking
2165         IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
2166
2167             SELECT  INTO result_row.asources COUNT(DISTINCT b.id)
2168               FROM  biblio.record_entry b
2169                     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
2170               WHERE b.id = ANY(all_arecords[1:browse_superpage_size])
2171                     AND (
2172                         acvac.vis_attr_vector @@ c_tests::query_int
2173                         OR b.vis_attr_vector @@ b_tests::query_int
2174                     );
2175
2176             result_row.aaccurate := TRUE;
2177
2178         END IF;
2179
2180         IF result_row.sources > 0 OR result_row.asources > 0 THEN
2181
2182             -- The function that calls this function needs row_number in order
2183             -- to correctly order results from two different runs of this
2184             -- functions.
2185             result_row.row_number := row_number;
2186
2187             -- Now, if row_counter is still less than limit, return a row.  If
2188             -- not, but it is less than next_pivot_pos, continue on without
2189             -- returning actual result rows until we find
2190             -- that next pivot, and return it.
2191
2192             IF row_counter < result_limit THEN
2193                 result_row.browse_entry := rec.id;
2194                 result_row.value := rec.value;
2195
2196                 RETURN NEXT result_row;
2197             ELSE
2198                 result_row.browse_entry := NULL;
2199                 result_row.authorities := NULL;
2200                 result_row.fields := NULL;
2201                 result_row.value := NULL;
2202                 result_row.sources := NULL;
2203                 result_row.sees := NULL;
2204                 result_row.accurate := NULL;
2205                 result_row.aaccurate := NULL;
2206                 result_row.pivot_point := rec.id;
2207
2208                 IF row_counter >= next_pivot_pos THEN
2209                     RETURN NEXT result_row;
2210                     RETURN;
2211                 END IF;
2212             END IF;
2213
2214             IF count_up_from_zero THEN
2215                 row_number := row_number + 1;
2216             ELSE
2217                 row_number := row_number - 1;
2218             END IF;
2219
2220             -- row_counter is different from row_number.
2221             -- It simply counts up from zero so that we know when
2222             -- we've reached our limit.
2223             row_counter := row_counter + 1;
2224         END IF;
2225     END LOOP;
2226 END;
2227 $f$ LANGUAGE plpgsql ROWS 10;
2228
2229 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON biblio.peer_bib_copy_map;
2230 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON biblio.record_entry;
2231 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.copy;
2232 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.call_number;
2233 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.copy_location;
2234 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON serial.unit;
2235 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON config.copy_status;
2236 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON actor.org_unit;
2237
2238 -- Upgrade the data!
2239 INSERT INTO asset.copy_vis_attr_cache (target_copy, record, vis_attr_vector)
2240     SELECT  cp.id,
2241             cn.record,
2242             asset.calculate_copy_visibility_attribute_set(cp.id)
2243       FROM  asset.copy cp
2244             JOIN asset.call_number cn ON (cp.call_number = cn.id);
2245
2246 -- updating vis cache for biblio.record_entry deferred to end
2247
2248 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();
2249 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();
2250 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER UPDATE ON asset.call_number FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2251 CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2252 CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2253 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2254 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
2255
2256 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$
2257 DECLARE
2258     ans RECORD;
2259     trans INT;
2260 BEGIN
2261     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;
2262
2263     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
2264         RETURN QUERY
2265         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
2266              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
2267              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
2268         SELECT  ans.depth,
2269                 ans.id,
2270                 COUNT( av.id ),
2271                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
2272                 COUNT( av.id ),
2273                 trans
2274           FROM  mask,
2275                 available_statuses,
2276                 org_list,
2277                 asset.copy_vis_attr_cache av
2278                 JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
2279           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
2280           GROUP BY 1,2,6;
2281
2282         IF NOT FOUND THEN
2283             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
2284         END IF;
2285
2286     END LOOP;
2287
2288     RETURN;
2289 END;
2290 $f$ LANGUAGE PLPGSQL;
2291
2292 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$
2293 DECLARE
2294     ans RECORD;
2295     trans INT;
2296 BEGIN
2297     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;
2298
2299     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
2300         RETURN QUERY
2301         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
2302              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
2303              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
2304         SELECT  -1,
2305                 ans.id,
2306                 COUNT( av.id ),
2307                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
2308                 COUNT( av.id ),
2309                 trans
2310           FROM  mask,
2311                 org_list,
2312                 asset.copy_vis_attr_cache av
2313                 JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
2314           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
2315           GROUP BY 1,2,6;
2316
2317         IF NOT FOUND THEN
2318             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
2319         END IF;
2320
2321     END LOOP;
2322
2323     RETURN;
2324 END;
2325 $f$ LANGUAGE PLPGSQL;
2326
2327 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$
2328 DECLARE
2329     ans RECORD;
2330     trans INT;
2331 BEGIN
2332     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;
2333
2334     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
2335         RETURN QUERY
2336         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
2337              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
2338              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
2339         SELECT  ans.depth,
2340                 ans.id,
2341                 COUNT( av.id ),
2342                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
2343                 COUNT( av.id ),
2344                 trans
2345           FROM  mask,
2346                 org_list,
2347                 available_statuses,
2348                 asset.copy_vis_attr_cache av
2349                 JOIN asset.copy cp ON (cp.id = av.target_copy)
2350                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
2351           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
2352           GROUP BY 1,2,6;
2353
2354         IF NOT FOUND THEN
2355             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
2356         END IF;
2357
2358     END LOOP;
2359
2360     RETURN;
2361 END;
2362 $f$ LANGUAGE PLPGSQL;
2363
2364 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$
2365 DECLARE
2366     ans RECORD;
2367     trans INT;
2368 BEGIN
2369     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;
2370
2371     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
2372         RETURN QUERY
2373         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
2374              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
2375              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
2376         SELECT  -1,
2377                 ans.id,
2378                 COUNT( av.id ),
2379                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
2380                 COUNT( av.id ),
2381                 trans
2382           FROM  mask,
2383                 org_list,
2384                 available_statuses,
2385                 asset.copy_vis_attr_cache av
2386                 JOIN asset.copy cp ON (cp.id = av.target_copy)
2387                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
2388           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
2389           GROUP BY 1,2,6;
2390
2391         IF NOT FOUND THEN
2392             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
2393         END IF;
2394
2395     END LOOP;
2396
2397     RETURN;
2398 END;
2399 $f$ LANGUAGE PLPGSQL;
2400
2401 CREATE OR REPLACE FUNCTION unapi.mmr_mra (
2402     obj_id BIGINT,
2403     format TEXT,
2404     ename TEXT,
2405     includes TEXT[],
2406     org TEXT,
2407     depth INT DEFAULT NULL,
2408     slimit HSTORE DEFAULT NULL,
2409     soffset HSTORE DEFAULT NULL,
2410     include_xmlns BOOL DEFAULT TRUE,
2411     pref_lib INT DEFAULT NULL
2412 ) RETURNS XML AS $F$
2413     SELECT  XMLELEMENT(
2414         name attributes,
2415         XMLATTRIBUTES(
2416             CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
2417             'tag:open-ils.org:U2@mmr/' || $1 AS metarecord
2418         ),
2419         (SELECT XMLAGG(foo.y)
2420           FROM (
2421             WITH sourcelist AS (
2422                 WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id FROM actor.org_unit WHERE shortname = $5 LIMIT 1),
2423                      basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
2424                      circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY_AGG(aoud.id)) AS mask
2425                                   FROM aou, LATERAL actor.org_unit_descendants(aou.id, $6) aoud)
2426                 SELECT  source
2427                   FROM  aou, circvm, basevm, metabib.metarecord_source_map mmsm
2428                   WHERE mmsm.metarecord = $1 AND (
2429                     EXISTS (
2430                         SELECT  1
2431                           FROM  circvm, basevm, asset.copy_vis_attr_cache acvac
2432                           WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
2433                                 AND acvac.record = mmsm.source
2434                     )
2435                     OR EXISTS (SELECT 1 FROM evergreen.located_uris(source, aou.id, $10) LIMIT 1)
2436                     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)
2437                 )
2438             )
2439             SELECT  cmra.aid,
2440                     XMLELEMENT(
2441                         name field,
2442                         XMLATTRIBUTES(
2443                             cmra.attr AS name,
2444                             cmra.value AS "coded-value",
2445                             cmra.aid AS "cvmid",
2446                             rad.composite,
2447                             rad.multi,
2448                             rad.filter,
2449                             rad.sorter,
2450                             cmra.source_list
2451                         ),
2452                         cmra.value
2453                     )
2454               FROM  (
2455                 SELECT DISTINCT aid, attr, value, STRING_AGG(x.id::TEXT, ',') AS source_list
2456                   FROM (
2457                     SELECT  v.source AS id,
2458                             c.id AS aid,
2459                             c.ctype AS attr,
2460                             c.code AS value
2461                       FROM  metabib.record_attr_vector_list v
2462                             JOIN config.coded_value_map c ON ( c.id = ANY( v.vlist ) )
2463                     ) AS x
2464                     JOIN sourcelist ON (x.id = sourcelist.source)
2465                     GROUP BY 1, 2, 3
2466                 ) AS cmra
2467                 JOIN config.record_attr_definition rad ON (cmra.attr = rad.name)
2468                 UNION ALL
2469             SELECT  umra.aid,
2470                     XMLELEMENT(
2471                         name field,
2472                         XMLATTRIBUTES(
2473                             umra.attr AS name,
2474                             rad.composite,
2475                             rad.multi,
2476                             rad.filter,
2477                             rad.sorter
2478                         ),
2479                         umra.value
2480                     )
2481               FROM  (
2482                 SELECT DISTINCT aid, attr, value
2483                   FROM (
2484                     SELECT  v.source AS id,
2485                             m.id AS aid,
2486                             m.attr AS attr,
2487                             m.value AS value
2488                       FROM  metabib.record_attr_vector_list v
2489                             JOIN metabib.uncontrolled_record_attr_value m ON ( m.id = ANY( v.vlist ) )
2490                     ) AS x
2491                     JOIN sourcelist ON (x.id = sourcelist.source)
2492                 ) AS umra
2493                 JOIN config.record_attr_definition rad ON (umra.attr = rad.name)
2494                 ORDER BY 1
2495
2496             )foo(id,y)
2497         )
2498     )
2499 $F$ LANGUAGE SQL STABLE;
2500
2501 CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
2502     bibid BIGINT[],
2503     ouid INT,
2504     depth INT DEFAULT NULL,
2505     slimit HSTORE DEFAULT NULL,
2506     soffset HSTORE DEFAULT NULL,
2507     pref_lib INT DEFAULT NULL,
2508     includes TEXT[] DEFAULT NULL::TEXT[]
2509 ) RETURNS TABLE(id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT) AS $$
2510     WITH RECURSIVE ou_depth AS (
2511         SELECT COALESCE(
2512             $3,
2513             (
2514                 SELECT depth
2515                 FROM actor.org_unit_type aout
2516                     INNER JOIN actor.org_unit ou ON ou_type = aout.id
2517                 WHERE ou.id = $2
2518             )
2519         ) AS depth
2520     ), descendant_depth AS (
2521         SELECT  ou.id,
2522                 ou.parent_ou,
2523                 out.depth
2524         FROM  actor.org_unit ou
2525                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2526                 JOIN anscestor_depth ad ON (ad.id = ou.id),
2527                 ou_depth
2528         WHERE ad.depth = ou_depth.depth
2529             UNION ALL
2530         SELECT  ou.id,
2531                 ou.parent_ou,
2532                 out.depth
2533         FROM  actor.org_unit ou
2534                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2535                 JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
2536     ), anscestor_depth AS (
2537         SELECT  ou.id,
2538                 ou.parent_ou,
2539                 out.depth
2540         FROM  actor.org_unit ou
2541                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2542         WHERE ou.id = $2
2543             UNION ALL
2544         SELECT  ou.id,
2545                 ou.parent_ou,
2546                 out.depth
2547         FROM  actor.org_unit ou
2548                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
2549                 JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
2550     ), descendants as (
2551         SELECT ou.* FROM actor.org_unit ou JOIN descendant_depth USING (id)
2552     )
2553
2554     SELECT ua.id, ua.name, ua.label_sortkey, MIN(ua.rank) AS rank FROM (
2555         SELECT acn.id, owning_lib.name, acn.label_sortkey,
2556             evergreen.rank_cp(acp),
2557             RANK() OVER w
2558         FROM asset.call_number acn
2559             JOIN asset.copy acp ON (acn.id = acp.call_number)
2560             JOIN descendants AS aou ON (acp.circ_lib = aou.id)
2561             JOIN actor.org_unit AS owning_lib ON (acn.owning_lib = owning_lib.id)
2562         WHERE acn.record = ANY ($1)
2563             AND acn.deleted IS FALSE
2564             AND acp.deleted IS FALSE
2565             AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN
2566                 EXISTS (
2567                     WITH basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
2568                          circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY[acp.circ_lib]) AS mask)
2569                     SELECT  1
2570                       FROM  basevm, circvm, asset.copy_vis_attr_cache acvac
2571                       WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
2572                             AND acvac.target_copy = acp.id
2573                             AND acvac.record = acn.record
2574                 ) ELSE TRUE END
2575         GROUP BY acn.id, evergreen.rank_cp(acp), owning_lib.name, acn.label_sortkey, aou.id
2576         WINDOW w AS (
2577             ORDER BY
2578                 COALESCE(
2579                     CASE WHEN aou.id = $2 THEN -20000 END,
2580                     CASE WHEN aou.id = $6 THEN -10000 END,
2581                     (SELECT distance - 5000
2582                         FROM actor.org_unit_descendants_distance($6) as x
2583                         WHERE x.id = aou.id AND $6 IN (
2584                             SELECT q.id FROM actor.org_unit_descendants($2) as q)),
2585                     (SELECT e.distance FROM actor.org_unit_descendants_distance($2) as e WHERE e.id = aou.id),
2586                     1000
2587                 ),
2588                 evergreen.rank_cp(acp)
2589         )
2590     ) AS ua
2591     GROUP BY ua.id, ua.name, ua.label_sortkey
2592     ORDER BY rank, ua.name, ua.label_sortkey
2593     LIMIT ($4 -> 'acn')::INT
2594     OFFSET ($5 -> 'acn')::INT;
2595 $$ LANGUAGE SQL STABLE ROWS 10;
2596
2597
2598 -- Evergreen DB patch XXXX.schema.action-trigger.event_definition.sms_preminder.sql
2599 --
2600 -- New action trigger event definition: 3 Day Courtesy Notice by SMS
2601 --
2602
2603 -- check whether patch can be applied
2604 SELECT evergreen.upgrade_deps_block_check('1058', :eg_version); -- mccanna/csharp/gmcharlt
2605
2606 INSERT INTO action_trigger.event_definition (id, active, owner, name, hook,
2607         validator, reactor, delay, max_delay, delay_field, group_field, template)
2608     VALUES (54, FALSE, 1,
2609         '3 Day Courtesy Notice by SMS',
2610         'checkout.due',
2611         'CircIsOpen', 'SendSMS', '-3 days', '-2 days', 'due_date', 'usr',
2612 $$
2613 [%- USE date -%]
2614 [%- user = target.0.usr -%]
2615 [%- homelib = user.home_ou -%]
2616 [%- sms_number = helpers.get_user_setting(user.id, 'opac.default_sms_notify') -%]
2617 [%- sms_carrier = helpers.get_user_setting(user.id, 'opac.default_sms_carrier') -%]
2618 From: [%- helpers.get_org_setting(homelib.id, 'org.bounced_emails') || homelib.email || params.sender_email || default_sender %]
2619 To: [%- helpers.get_sms_gateway_email(sms_carrier,sms_number) %]
2620 Subject: Library Materials Due Soon
2621
2622 You have items due soon:
2623
2624 [% FOR circ IN target %]
2625 [%- copy_details = helpers.get_copy_bib_basics(circ.target_copy.id) -%]
2626 [% copy_details.title FILTER ucfirst %] by [% copy_details.author FILTER ucfirst %] due on [% date.format(helpers.format_date(circ.due_date), '%m-%d-%Y') %]
2627
2628 [% END %]
2629
2630 $$);
2631
2632 INSERT INTO action_trigger.environment (event_def, path) VALUES
2633     (54, 'circ_lib.billing_address'),
2634     (54, 'target_copy.call_number'),
2635     (54, 'usr'),
2636     (54, 'usr.home_ou');
2637
2638
2639 -- check whether patch can be applied
2640 SELECT evergreen.upgrade_deps_block_check('1059', :eg_version); --Stompro/DPearl/kmlussier
2641
2642 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
2643 SELECT  r.id,
2644     r.fingerprint,
2645     r.quality,
2646     r.tcn_source,
2647     r.tcn_value,
2648     CONCAT_WS(' ', FIRST(title.value),FIRST(title_np.val)) AS title,
2649     FIRST(author.value) AS author,
2650     STRING_AGG(DISTINCT publisher.value, ', ') AS publisher,
2651     STRING_AGG(DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$), ', ') AS pubdate,
2652     CASE WHEN ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) = '{NULL}'
2653         THEN NULL
2654         ELSE ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') )
2655     END AS isbn,
2656     CASE WHEN ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) = '{NULL}'
2657         THEN NULL
2658         ELSE ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') )
2659     END AS issn
2660   FROM  biblio.record_entry r
2661     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
2662     LEFT JOIN ( -- Grab 245 N and P subfields in the order that they appear.
2663       SELECT b.record, string_agg(val, ' ') AS val FROM (
2664              SELECT title_np.record, title_np.value AS val
2665               FROM metabib.full_rec title_np
2666               WHERE
2667               title_np.tag = '245'
2668                         AND title_np.subfield IN ('p','n')                      
2669                         ORDER BY title_np.id
2670                 ) b
2671                 GROUP BY 1
2672          ) title_np ON (title_np.record=r.id) 
2673     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
2674     LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND (publisher.tag = '260' OR (publisher.tag = '264' AND publisher.ind2 = '1')) AND publisher.subfield = 'b')
2675     LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND (pubdate.tag = '260' OR (pubdate.tag = '264' AND pubdate.ind2 = '1')) AND pubdate.subfield = 'c')
2676     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
2677     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
2678   GROUP BY 1,2,3,4,5;
2679
2680   
2681   -- Remove trigger on biblio.record_entry
2682   SELECT reporter.disable_materialized_simple_record_trigger();
2683   
2684   -- Rebuild reporter.materialized_simple_record
2685   SELECT reporter.enable_materialized_simple_record_trigger();
2686   
2687
2688 SELECT evergreen.upgrade_deps_block_check('1060', :eg_version);
2689
2690 DROP VIEW IF EXISTS extend_reporter.copy_count_per_org;
2691
2692
2693 CREATE OR REPLACE VIEW extend_reporter.copy_count_per_org AS
2694  SELECT acn.record AS bibid,
2695     ac.circ_lib,
2696     acn.owning_lib,
2697     max(ac.edit_date) AS last_edit_time,
2698     min(ac.deleted::integer) AS has_only_deleted_copies,
2699     count(
2700         CASE
2701             WHEN ac.deleted THEN ac.id
2702             ELSE NULL::bigint
2703         END) AS deleted_count,
2704     count(
2705         CASE
2706             WHEN NOT ac.deleted THEN ac.id
2707             ELSE NULL::bigint
2708         END) AS visible_count,
2709     count(*) AS total_count
2710    FROM asset.call_number acn,
2711     asset.copy ac
2712   WHERE ac.call_number = acn.id
2713   GROUP BY acn.record, acn.owning_lib, ac.circ_lib;
2714
2715
2716
2717 SELECT evergreen.upgrade_deps_block_check('1061', :eg_version);
2718
2719 INSERT INTO config.org_unit_setting_type
2720     (name, label, description, grp, datatype)
2721 VALUES (
2722     'ui.staff.max_recent_patrons',
2723     oils_i18n_gettext(
2724         'ui.staff.max_recent_patrons',
2725         'Number of Retrievable Recent Patrons',
2726         'coust',
2727         'label'
2728     ),
2729     oils_i18n_gettext(
2730         'ui.staff.max_recent_patrons',
2731         'Number of most recently accessed patrons that can be re-retrieved ' ||
2732         'in the staff client.  A value of 0 or less disables the feature. Defaults to 1.',
2733         'coust',
2734         'description'
2735     ),
2736     'circ',
2737     'integer'
2738 );
2739
2740
2741 SELECT evergreen.upgrade_deps_block_check('1062', :eg_version);
2742
2743 CREATE TABLE acq.edi_attr (
2744     key     TEXT PRIMARY KEY,
2745     label   TEXT NOT NULL UNIQUE
2746 );
2747
2748 CREATE TABLE acq.edi_attr_set (
2749     id      SERIAL  PRIMARY KEY,
2750     label   TEXT NOT NULL UNIQUE
2751 );
2752
2753 CREATE TABLE acq.edi_attr_set_map (
2754     id          SERIAL  PRIMARY KEY,
2755     attr_set    INTEGER NOT NULL REFERENCES acq.edi_attr_set(id) 
2756                 ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2757     attr        TEXT NOT NULL REFERENCES acq.edi_attr(key) 
2758                 ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2759     CONSTRAINT edi_attr_set_map_attr_once UNIQUE (attr_set, attr)
2760 );
2761
2762 -- An attr_set is not strictly required, since some edi_accounts/vendors 
2763 -- may not need to apply any attributes.
2764 ALTER TABLE acq.edi_account 
2765     ADD COLUMN attr_set INTEGER REFERENCES acq.edi_attr_set(id),
2766     ADD COLUMN use_attrs BOOLEAN NOT NULL DEFAULT FALSE;
2767
2768
2769
2770
2771 SELECT evergreen.upgrade_deps_block_check('1063', :eg_version);
2772
2773 DO $temp$
2774 DECLARE
2775         r RECORD;
2776 BEGIN
2777
2778         FOR r IN SELECT t.table_schema AS sname,
2779                         t.table_name AS tname,
2780                         t.column_name AS colname,
2781                         t.constraint_name
2782                   FROM  information_schema.referential_constraints ref
2783                         JOIN information_schema.key_column_usage t USING (constraint_schema,constraint_name)
2784                   WHERE ref.unique_constraint_schema = 'asset'
2785                         AND ref.unique_constraint_name = 'copy_pkey'
2786         LOOP
2787
2788                 EXECUTE 'ALTER TABLE '||r.sname||'.'||r.tname||' DROP CONSTRAINT '||r.constraint_name||';';
2789
2790                 EXECUTE '
2791                         CREATE OR REPLACE FUNCTION evergreen.'||r.sname||'_'||r.tname||'_'||r.colname||'_inh_fkey() RETURNS TRIGGER AS $f$
2792                         BEGIN
2793                                 PERFORM 1 FROM asset.copy WHERE id = NEW.'||r.colname||';
2794                                 IF NOT FOUND THEN
2795                                         RAISE foreign_key_violation USING MESSAGE = FORMAT(
2796                                                 $$Referenced asset.copy id not found, '||r.colname||':%s$$, NEW.'||r.colname||'
2797                                         );
2798                                 END IF;
2799                                 RETURN NEW;
2800                         END;
2801                         $f$ LANGUAGE PLPGSQL VOLATILE COST 50;
2802                 ';
2803
2804                 EXECUTE '
2805                         CREATE CONSTRAINT TRIGGER inherit_'||r.constraint_name||'
2806                                 AFTER UPDATE OR INSERT OR DELETE ON '||r.sname||'.'||r.tname||'
2807                                 DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE evergreen.'||r.sname||'_'||r.tname||'_'||r.colname||'_inh_fkey();
2808                 ';
2809         END LOOP;
2810 END
2811 $temp$;
2812
2813
2814
2815 SELECT evergreen.upgrade_deps_block_check('1064', :eg_version);
2816
2817 ALTER TABLE serial.issuance DROP CONSTRAINT IF EXISTS issuance_caption_and_pattern_fkey;
2818
2819 -- Using NOT VALID and VALIDATE CONSTRAINT limits the impact to concurrent work.
2820 -- For details, see: https://www.postgresql.org/docs/current/static/sql-altertable.html
2821
2822 ALTER TABLE serial.issuance ADD CONSTRAINT issuance_caption_and_pattern_fkey
2823     FOREIGN KEY (caption_and_pattern)
2824     REFERENCES serial.caption_and_pattern (id)
2825     ON DELETE CASCADE
2826     DEFERRABLE INITIALLY DEFERRED
2827     NOT VALID;
2828
2829 ALTER TABLE serial.issuance VALIDATE CONSTRAINT issuance_caption_and_pattern_fkey;
2830
2831
2832
2833 SELECT evergreen.upgrade_deps_block_check('1065', :eg_version);
2834
2835 CREATE TABLE serial.pattern_template (
2836     id            SERIAL PRIMARY KEY,
2837     name          TEXT NOT NULL,
2838     pattern_code  TEXT NOT NULL,
2839     owning_lib    INTEGER REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED,
2840     share_depth   INTEGER NOT NULL DEFAULT 0
2841 );
2842 CREATE INDEX serial_pattern_template_name_idx ON serial.pattern_template (evergreen.lowercase(name));
2843
2844 CREATE OR REPLACE FUNCTION serial.pattern_templates_visible_to(org_unit INT) RETURNS SETOF serial.pattern_template AS $func$
2845 BEGIN
2846     RETURN QUERY SELECT *
2847            FROM serial.pattern_template spt
2848            WHERE (
2849              SELECT ARRAY_AGG(id)
2850              FROM actor.org_unit_descendants(spt.owning_lib, spt.share_depth)
2851            ) @@ org_unit::TEXT::QUERY_INT;
2852 END;
2853 $func$ LANGUAGE PLPGSQL;
2854
2855
2856 SELECT evergreen.upgrade_deps_block_check('1066', :eg_version);
2857
2858 INSERT INTO permission.perm_list ( id, code, description ) VALUES
2859  ( 593, 'ADMIN_SERIAL_PATTERN_TEMPLATE', oils_i18n_gettext( 593,
2860     'Administer serial prediction pattern templates', 'ppl', 'description' ))
2861 ;
2862
2863 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
2864     SELECT
2865         pgt.id, perm.id, aout.depth, FALSE
2866     FROM
2867         permission.grp_tree pgt,
2868         permission.perm_list perm,
2869         actor.org_unit_type aout
2870     WHERE
2871         pgt.name = 'Serials' AND
2872         aout.name = 'System' AND
2873         perm.code IN (
2874             'ADMIN_SERIAL_PATTERN_TEMPLATE'
2875         );
2876
2877
2878 SELECT evergreen.upgrade_deps_block_check('1067', :eg_version);
2879
2880 INSERT INTO acq.edi_attr (key, label) VALUES
2881     ('INCLUDE_PO_NAME', 
2882         oils_i18n_gettext('INCLUDE_PO_NAME', 
2883         'Orders Include PO Name', 'aea', 'label')),
2884     ('INCLUDE_COPIES', 
2885         oils_i18n_gettext('INCLUDE_COPIES', 
2886         'Orders Include Copy Data', 'aea', 'label')),
2887     ('INCLUDE_FUND', 
2888         oils_i18n_gettext('INCLUDE_FUND', 
2889         'Orders Include Copy Funds', 'aea', 'label')),
2890     ('INCLUDE_CALL_NUMBER', 
2891         oils_i18n_gettext('INCLUDE_CALL_NUMBER', 
2892         'Orders Include Copy Call Numbers', 'aea', 'label')),
2893     ('INCLUDE_ITEM_TYPE', 
2894         oils_i18n_gettext('INCLUDE_ITEM_TYPE', 
2895         'Orders Include Copy Item Types', 'aea', 'label')),
2896     ('INCLUDE_ITEM_BARCODE',
2897         oils_i18n_gettext('INCLUDE_ITEM_BARCODE',
2898         'Orders Include Copy Barcodes', 'aea', 'label')),
2899     ('INCLUDE_LOCATION', 
2900         oils_i18n_gettext('INCLUDE_LOCATION', 
2901         'Orders Include Copy Locations', 'aea', 'label')),
2902     ('INCLUDE_COLLECTION_CODE', 
2903         oils_i18n_gettext('INCLUDE_COLLECTION_CODE', 
2904         'Orders Include Copy Collection Codes', 'aea', 'label')),
2905     ('INCLUDE_OWNING_LIB', 
2906         oils_i18n_gettext('INCLUDE_OWNING_LIB', 
2907         'Orders Include Copy Owning Library', 'aea', 'label')),
2908     ('USE_ID_FOR_OWNING_LIB',
2909         oils_i18n_gettext('USE_ID_FOR_OWNING_LIB',
2910         'Emit Owning Library ID Rather Than Short Name. Takes effect only if INCLUDE_OWNING_LIB is in use', 'aea', 'label')),
2911     ('INCLUDE_QUANTITY', 
2912         oils_i18n_gettext('INCLUDE_QUANTITY', 
2913         'Orders Include Copy Quantities', 'aea', 'label')),
2914     ('INCLUDE_COPY_ID', 
2915         oils_i18n_gettext('INCLUDE_COPY_ID', 
2916         'Orders Include Copy IDs', 'aea', 'label')),
2917     ('BUYER_ID_INCLUDE_VENDCODE', 
2918         oils_i18n_gettext('BUYER_ID_INCLUDE_VENDCODE', 
2919         'Buyer ID Qualifier Includes Vendcode', 'aea', 'label')),
2920     ('BUYER_ID_ONLY_VENDCODE', 
2921         oils_i18n_gettext('BUYER_ID_ONLY_VENDCODE', 
2922         'Buyer ID Qualifier Only Contains Vendcode', 'aea', 'label')),
2923     ('INCLUDE_BIB_EDITION', 
2924         oils_i18n_gettext('INCLUDE_BIB_EDITION', 
2925         'Order Lineitems Include Edition Info', 'aea', 'label')),
2926     ('INCLUDE_BIB_AUTHOR', 
2927         oils_i18n_gettext('INCLUDE_BIB_AUTHOR', 
2928         'Order Lineitems Include Author Info', 'aea', 'label')),
2929     ('INCLUDE_BIB_PAGINATION', 
2930         oils_i18n_gettext('INCLUDE_BIB_PAGINATION', 
2931         'Order Lineitems Include Pagination Info', 'aea', 'label')),
2932     ('COPY_SPEC_CODES', 
2933         oils_i18n_gettext('COPY_SPEC_CODES', 
2934         'Order Lineitem Notes Include Copy Spec Codes', 'aea', 'label')),
2935     ('INCLUDE_EMPTY_IMD_VALUES', 
2936         oils_i18n_gettext('INCLUDE_EMPTY_IMD_VALUES',
2937         'Lineitem Title, Author, etc. Fields Are Present Even if Empty', 'aea', 'label')),
2938     ('INCLUDE_EMPTY_LI_NOTE', 
2939         oils_i18n_gettext('INCLUDE_EMPTY_LI_NOTE', 
2940         'Order Lineitem Notes Always Present (Even if Empty)', 'aea', 'label')),
2941     ('INCLUDE_EMPTY_CALL_NUMBER', 
2942         oils_i18n_gettext('INCLUDE_EMPTY_CALL_NUMBER', 
2943         'Order Copies Always Include Call Number (Even if Empty)', 'aea', 'label')),
2944     ('INCLUDE_EMPTY_ITEM_TYPE', 
2945         oils_i18n_gettext('INCLUDE_EMPTY_ITEM_TYPE', 
2946         'Order Copies Always Include Item Type (Even if Empty)', 'aea', 'label')),
2947     ('INCLUDE_EMPTY_LOCATION', 
2948         oils_i18n_gettext('INCLUDE_EMPTY_LOCATION', 
2949         'Order Copies Always Include Location (Even if Empty)', 'aea', 'label')),
2950     ('INCLUDE_EMPTY_COLLECTION_CODE', 
2951         oils_i18n_gettext('INCLUDE_EMPTY_COLLECTION_CODE', 
2952         'Order Copies Always Include Collection Code (Even if Empty)', 'aea', 'label')),
2953     ('LINEITEM_IDENT_VENDOR_NUMBER',
2954         oils_i18n_gettext('LINEITEM_IDENT_VENDOR_NUMBER',
2955         'Lineitem Identifier Fields (LIN/PIA) Use Vendor-Encoded ID Value When Available', 'aea', 'label')),
2956     ('LINEITEM_REF_ID_ONLY',
2957         oils_i18n_gettext('LINEITEM_REF_ID_ONLY',
2958         'Lineitem Reference Field (RFF) Uses Lineitem ID Only', 'aea', 'label'))
2959
2960 ;
2961
2962 INSERT INTO acq.edi_attr_set (id, label) VALUES (1, 'Ingram Default');
2963 INSERT INTO acq.edi_attr_set (id, label) VALUES (2, 'Baker & Taylor Default');
2964 INSERT INTO acq.edi_attr_set (id, label) VALUES (3, 'Brodart Default');
2965 INSERT INTO acq.edi_attr_set (id, label) VALUES (4, 'Midwest Tape Default');
2966 INSERT INTO acq.edi_attr_set (id, label) VALUES (5, 'ULS Default');
2967 INSERT INTO acq.edi_attr_set (id, label) VALUES (6, 'Recorded Books Default');
2968 INSERT INTO acq.edi_attr_set (id, label) VALUES (7, 'Midwest Library Service');
2969
2970 -- carve out space for mucho defaults
2971 SELECT SETVAL('acq.edi_attr_set_id_seq'::TEXT, 1000);
2972
2973 INSERT INTO acq.edi_attr_set_map (attr_set, attr) VALUES
2974
2975     -- Ingram
2976     (1, 'INCLUDE_PO_NAME'),
2977     (1, 'INCLUDE_COPIES'),
2978     (1, 'INCLUDE_ITEM_TYPE'),
2979     (1, 'INCLUDE_COLLECTION_CODE'),
2980     (1, 'INCLUDE_OWNING_LIB'),
2981     (1, 'INCLUDE_QUANTITY'),
2982     (1, 'INCLUDE_BIB_PAGINATION'),