]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/upgrade/1057.schema.copy_vis_attr_cache.sql
LP1894131 Sticky catalog holdings org select
[Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / 1057.schema.copy_vis_attr_cache.sql
1 BEGIN;
2
3 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1057', :eg_version); -- miker/gmcharlt/kmlussier
4
5 -- Thist change drops a needless join and saves 10-15% in time cost
6 CREATE OR REPLACE FUNCTION search.facets_for_record_set(ignore_facet_classes text[], hits bigint[]) RETURNS TABLE(id integer, value text, count bigint)
7 AS $f$
8     SELECT id, value, count
9       FROM (
10         SELECT  mfae.field AS id,
11                 mfae.value,
12                 COUNT(DISTINCT mfae.source),
13                 row_number() OVER (
14                     PARTITION BY mfae.field ORDER BY COUNT(DISTINCT mfae.source) DESC
15                 ) AS rownum
16           FROM  metabib.facet_entry mfae
17                 JOIN config.metabib_field cmf ON (cmf.id = mfae.field)
18           WHERE mfae.source = ANY ($2)
19                 AND cmf.facet_field
20                 AND cmf.field_class NOT IN (SELECT * FROM unnest($1))
21           GROUP by 1, 2
22       ) all_facets
23       WHERE rownum <= (
24         SELECT COALESCE(
25             (SELECT value::INT FROM config.global_flag WHERE name = 'search.max_facets_per_field' AND enabled),
26             1000
27         )
28       );
29 $f$ LANGUAGE SQL;
30
31 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$
32 DECLARE
33     layout          unapi.bre_output_layout%ROWTYPE;
34     transform       config.xml_transform%ROWTYPE;
35     item_format     TEXT;
36     tmp_xml         TEXT;
37     xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
38     ouid            INT;
39     element_list    TEXT[];
40 BEGIN
41
42     IF org = '-' OR org IS NULL THEN
43         SELECT shortname INTO org FROM evergreen.org_top();
44     END IF;
45
46     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
47     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
48
49     IF layout.name IS NULL THEN
50         RETURN NULL::XML;
51     END IF;
52
53     SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
54     xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
55
56     -- Gather the bib xml
57     SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
58
59     IF layout.title_element IS NOT NULL THEN
60         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
61     END IF;
62
63     IF layout.description_element IS NOT NULL THEN
64         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
65     END IF;
66
67     IF layout.creator_element IS NOT NULL THEN
68         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
69     END IF;
70
71     IF layout.update_ts_element IS NOT NULL THEN
72         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;
73     END IF;
74
75     IF unapi_url IS NOT NULL THEN
76         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;
77     END IF;
78
79     IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
80
81     element_list := regexp_split_to_array(layout.feed_top,E'\\.');
82     FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
83         EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
84     END LOOP;
85
86     RETURN tmp_xml::XML;
87 END;
88 $F$ LANGUAGE PLPGSQL STABLE;
89
90 CREATE TABLE asset.copy_vis_attr_cache (
91     id              BIGSERIAL   PRIMARY KEY,
92     record          BIGINT      NOT NULL, -- No FKEYs, managed by user triggers.
93     target_copy     BIGINT      NOT NULL,
94     vis_attr_vector INT[]
95 );
96 CREATE INDEX copy_vis_attr_cache_record_idx ON asset.copy_vis_attr_cache (record);
97 CREATE INDEX copy_vis_attr_cache_copy_idx ON asset.copy_vis_attr_cache (target_copy);
98
99 ALTER TABLE biblio.record_entry ADD COLUMN vis_attr_vector INT[];
100
101 CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute ( value INT, attr TEXT ) RETURNS INT AS $f$
102 SELECT  ((CASE $2
103
104             WHEN 'luri_org'         THEN 0 -- "b" attr
105             WHEN 'bib_source'       THEN 1 -- "b" attr
106
107             WHEN 'copy_flags'       THEN 0 -- "c" attr
108             WHEN 'owning_lib'       THEN 1 -- "c" attr
109             WHEN 'circ_lib'         THEN 2 -- "c" attr
110             WHEN 'status'           THEN 3 -- "c" attr
111             WHEN 'location'         THEN 4 -- "c" attr
112             WHEN 'location_group'   THEN 5 -- "c" attr
113
114         END) << 28 ) | $1;
115
116 /* copy_flags bit positions, LSB-first:
117
118  0: asset.copy.opac_visible
119
120
121    When adding flags, you must update asset.all_visible_flags()
122
123    Because bib and copy values are stored separately, we can reuse
124    shifts, saving us some space. We could probably take back a bit
125    too, but I'm not sure its worth squeezing that last one out. We'd
126    be left with just 2 slots for copy attrs, rather than 10.
127 */
128
129 $f$ LANGUAGE SQL IMMUTABLE;
130
131 CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_list ( attr TEXT, value INT[] ) RETURNS INT[] AS $f$
132     SELECT ARRAY_AGG(search.calculate_visibility_attribute(x, $1)) FROM UNNEST($2) AS X;
133 $f$ LANGUAGE SQL IMMUTABLE;
134
135 CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_test ( attr TEXT, value INT[], negate BOOL DEFAULT FALSE ) RETURNS TEXT AS $f$
136     SELECT  CASE WHEN $3 THEN '!' ELSE '' END || '(' || ARRAY_TO_STRING(search.calculate_visibility_attribute_list($1,$2),'|') || ')';
137 $f$ LANGUAGE SQL IMMUTABLE;
138
139 CREATE OR REPLACE FUNCTION asset.calculate_copy_visibility_attribute_set ( copy_id BIGINT ) RETURNS INT[] AS $f$
140 DECLARE
141     copy_row    asset.copy%ROWTYPE;
142     lgroup_map  asset.copy_location_group_map%ROWTYPE;
143     attr_set    INT[];
144 BEGIN
145     SELECT * INTO copy_row FROM asset.copy WHERE id = copy_id;
146
147     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.opac_visible::INT, 'copy_flags');
148     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.circ_lib, 'circ_lib');
149     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.status, 'status');
150     attr_set := attr_set || search.calculate_visibility_attribute(copy_row.location, 'location');
151
152     SELECT  ARRAY_APPEND(
153                 attr_set,
154                 search.calculate_visibility_attribute(owning_lib, 'owning_lib')
155             ) INTO attr_set
156       FROM  asset.call_number
157       WHERE id = copy_row.call_number;
158
159     FOR lgroup_map IN SELECT * FROM asset.copy_location_group_map WHERE location = copy_row.location LOOP
160         attr_set := attr_set || search.calculate_visibility_attribute(lgroup_map.lgroup, 'location_group');
161     END LOOP;
162
163     RETURN attr_set;
164 END;
165 $f$ LANGUAGE PLPGSQL;
166
167 CREATE OR REPLACE FUNCTION biblio.calculate_bib_visibility_attribute_set ( bib_id BIGINT ) RETURNS INT[] AS $f$
168 DECLARE
169     bib_row     biblio.record_entry%ROWTYPE;
170     cn_row      asset.call_number%ROWTYPE;
171     attr_set    INT[];
172 BEGIN
173     SELECT * INTO bib_row FROM biblio.record_entry WHERE id = bib_id;
174
175     IF bib_row.source IS NOT NULL THEN
176         attr_set := attr_set || search.calculate_visibility_attribute(bib_row.source, 'bib_source');
177     END IF;
178
179     FOR cn_row IN
180         SELECT  cn.*
181           FROM  asset.call_number cn
182                 JOIN asset.uri_call_number_map m ON (cn.id = m.call_number)
183                 JOIN asset.uri u ON (u.id = m.uri)
184           WHERE cn.record = bib_id
185                 AND cn.label = '##URI##'
186                 AND u.active
187     LOOP
188         attr_set := attr_set || search.calculate_visibility_attribute(cn_row.owning_lib, 'luri_org');
189     END LOOP;
190
191     RETURN attr_set;
192 END;
193 $f$ LANGUAGE PLPGSQL;
194
195 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
196 DECLARE
197     ocn     asset.call_number%ROWTYPE;
198     ncn     asset.call_number%ROWTYPE;
199     cid     BIGINT;
200 BEGIN
201
202     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN -- Only needs ON INSERT OR DELETE, so handle separately
203         IF TG_OP = 'INSERT' THEN
204             INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
205                 NEW.peer_record,
206                 NEW.target_copy,
207                 asset.calculate_copy_visibility_attribute_set(NEW.target_copy)
208             );
209
210             RETURN NEW;
211         ELSIF TG_OP = 'DELETE' THEN
212             DELETE FROM asset.copy_vis_attr_cache
213               WHERE record = NEW.peer_record AND target_copy = NEW.target_copy;
214
215             RETURN OLD;
216         END IF;
217     END IF;
218
219     IF TG_OP = 'INSERT' THEN -- Handles ON INSERT. ON UPDATE is below.
220         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
221             SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
222             INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
223                 ncn.record,
224                 NEW.id,
225                 asset.calculate_copy_visibility_attribute_set(NEW.id)
226             );
227         ELSIF TG_TABLE_NAME = 'record_entry' THEN
228             NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
229         END IF;
230
231         RETURN NEW;
232     END IF;
233
234     -- handle items first, since with circulation activity
235     -- their statuses change frequently
236     IF TG_TABLE_NAME IN ('copy', 'unit') THEN -- This handles ON UPDATE OR DELETE. ON INSERT above
237
238         IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
239             DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
240             RETURN OLD;
241         END IF;
242
243         SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
244
245         IF OLD.deleted <> NEW.deleted THEN
246             IF NEW.deleted THEN
247                 DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
248             ELSE
249                 INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
250                     ncn.record,
251                     NEW.id,
252                     asset.calculate_copy_visibility_attribute_set(NEW.id)
253                 );
254             END IF;
255
256             RETURN NEW;
257         ELSIF OLD.call_number  <> NEW.call_number THEN
258             SELECT * INTO ocn FROM asset.call_number cn WHERE id = OLD.call_number;
259
260             IF ncn.record <> ocn.record THEN
261                 UPDATE  biblio.record_entry
262                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(ncn.record)
263                   WHERE id = ocn.record;
264             END IF;
265         END IF;
266
267         IF OLD.location     <> NEW.location OR
268            OLD.status       <> NEW.status OR
269            OLD.opac_visible <> NEW.opac_visible OR
270            OLD.circ_lib     <> NEW.circ_lib
271         THEN
272             -- any of these could change visibility, but
273             -- we'll save some queries and not try to calculate
274             -- the change directly
275             UPDATE  asset.copy_vis_attr_cache
276               SET   target_copy = NEW.id,
277                     vis_attr_vector = asset.calculate_copy_visibility_attribute_set(NEW.id)
278               WHERE target_copy = OLD.id;
279
280         END IF;
281
282     ELSIF TG_TABLE_NAME = 'call_number' THEN -- Only ON UPDATE. Copy handler will deal with ON INSERT OR DELETE.
283
284         IF OLD.record <> NEW.record THEN
285             IF NEW.label = '##URI##' THEN
286                 UPDATE  biblio.record_entry
287                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
288                   WHERE id = OLD.record;
289
290                 UPDATE  biblio.record_entry
291                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(NEW.record)
292                   WHERE id = NEW.record;
293             END IF;
294
295             UPDATE  asset.copy_vis_attr_cache
296               SET   record = NEW.record,
297                     vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
298               WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
299                     AND record = OLD.record;
300
301         ELSIF OLD.owning_lib <> NEW.owning_lib THEN
302             UPDATE  asset.copy_vis_attr_cache
303               SET   vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
304               WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
305                     AND record = NEW.record;
306
307             IF NEW.label = '##URI##' THEN
308                 UPDATE  biblio.record_entry
309                   SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
310                   WHERE id = OLD.record;
311             END IF;
312         END IF;
313
314     ELSIF TG_TABLE_NAME = 'record_entry' THEN -- Only handles ON UPDATE OR DELETE
315
316         IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
317             DELETE FROM asset.copy_vis_attr_cache WHERE record = OLD.id;
318             RETURN OLD;
319         ELSIF OLD.source <> NEW.source THEN
320             NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
321         END IF;
322
323     END IF;
324
325     RETURN NEW;
326 END;
327 $func$ LANGUAGE PLPGSQL;
328
329
330 -- Helper functions for use in constructing searches --
331
332 CREATE OR REPLACE FUNCTION asset.all_visible_flags () RETURNS TEXT AS $f$
333     SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(1 << x, 'copy_flags')),'&') || ')'
334       FROM  GENERATE_SERIES(0,0) AS x; -- increment as new flags are added.
335 $f$ LANGUAGE SQL STABLE;
336
337 CREATE OR REPLACE FUNCTION asset.visible_orgs (otype TEXT) RETURNS TEXT AS $f$
338     SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
339       FROM  actor.org_unit
340       WHERE opac_visible;
341 $f$ LANGUAGE SQL STABLE;
342
343 CREATE OR REPLACE FUNCTION asset.invisible_orgs (otype TEXT) RETURNS TEXT AS $f$
344     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
345       FROM  actor.org_unit
346       WHERE NOT opac_visible;
347 $f$ LANGUAGE SQL STABLE;
348
349 -- Bib-oriented defaults for search
350 CREATE OR REPLACE FUNCTION asset.bib_source_default () RETURNS TEXT AS $f$
351     SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'bib_source')),'|') || ')'
352       FROM  config.bib_source
353       WHERE transcendant;
354 $f$ LANGUAGE SQL IMMUTABLE;
355
356 CREATE OR REPLACE FUNCTION asset.luri_org_default () RETURNS TEXT AS $f$
357     SELECT  * FROM asset.invisible_orgs('luri_org');
358 $f$ LANGUAGE SQL STABLE;
359
360 -- Copy-oriented defaults for search
361 CREATE OR REPLACE FUNCTION asset.location_group_default () RETURNS TEXT AS $f$
362     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location_group')),'|') || ')'
363       FROM  asset.copy_location_group
364       WHERE NOT opac_visible;
365 $f$ LANGUAGE SQL STABLE;
366
367 CREATE OR REPLACE FUNCTION asset.location_default () RETURNS TEXT AS $f$
368     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location')),'|') || ')'
369       FROM  asset.copy_location
370       WHERE NOT opac_visible;
371 $f$ LANGUAGE SQL STABLE;
372
373 CREATE OR REPLACE FUNCTION asset.status_default () RETURNS TEXT AS $f$
374     SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'status')),'|') || ')'
375       FROM  config.copy_status
376       WHERE NOT opac_visible;
377 $f$ LANGUAGE SQL STABLE;
378
379 CREATE OR REPLACE FUNCTION asset.owning_lib_default () RETURNS TEXT AS $f$
380     SELECT  * FROM asset.invisible_orgs('owning_lib');
381 $f$ LANGUAGE SQL STABLE;
382
383 CREATE OR REPLACE FUNCTION asset.circ_lib_default () RETURNS TEXT AS $f$
384     SELECT  * FROM asset.invisible_orgs('circ_lib');
385 $f$ LANGUAGE SQL STABLE;
386
387 CREATE OR REPLACE FUNCTION asset.patron_default_visibility_mask () RETURNS TABLE (b_attrs TEXT, c_attrs TEXT)  AS $f$
388 DECLARE
389     copy_flags      TEXT; -- "c" attr
390
391     owning_lib      TEXT; -- "c" attr
392     circ_lib        TEXT; -- "c" attr
393     status          TEXT; -- "c" attr
394     location        TEXT; -- "c" attr
395     location_group  TEXT; -- "c" attr
396
397     luri_org        TEXT; -- "b" attr
398     bib_sources     TEXT; -- "b" attr
399 BEGIN
400     copy_flags      := asset.all_visible_flags(); -- Will always have at least one
401
402     owning_lib      := NULLIF(asset.owning_lib_default(),'!()');
403     
404     circ_lib        := NULLIF(asset.circ_lib_default(),'!()');
405     status          := NULLIF(asset.status_default(),'!()');
406     location        := NULLIF(asset.location_default(),'!()');
407     location_group  := NULLIF(asset.location_group_default(),'!()');
408
409     luri_org        := NULLIF(asset.luri_org_default(),'!()');
410     bib_sources     := NULLIF(asset.bib_source_default(),'()');
411
412     RETURN QUERY SELECT
413         '('||ARRAY_TO_STRING(
414             ARRAY[luri_org,bib_sources],
415             '|'
416         )||')',
417         '('||ARRAY_TO_STRING(
418             ARRAY[copy_flags,owning_lib,circ_lib,status,location,location_group]::TEXT[],
419             '&'
420         )||')';
421 END;
422 $f$ LANGUAGE PLPGSQL STABLE ROWS 1;
423
424 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)
425  RETURNS TABLE(value text, field integer, buoyant_and_class_match boolean, field_match boolean, field_weight integer, rank real, buoyant boolean, match text)
426 AS $f$
427 DECLARE
428     prepared_query_texts    TEXT[];
429     query                   TSQUERY;
430     plain_query             TSQUERY;
431     opac_visibility_join    TEXT;
432     search_class_join       TEXT;
433     r_fields                RECORD;
434 BEGIN
435     prepared_query_texts := metabib.autosuggest_prepare_tsquery(raw_query_text);
436
437     query := TO_TSQUERY('keyword', prepared_query_texts[1]);
438     plain_query := TO_TSQUERY('keyword', prepared_query_texts[2]);
439
440     visibility_org := NULLIF(visibility_org,-1);
441     IF visibility_org IS NOT NULL THEN
442         PERFORM FROM actor.org_unit WHERE id = visibility_org AND parent_ou IS NULL;
443         IF FOUND THEN
444             opac_visibility_join := '';
445         ELSE
446             opac_visibility_join := '
447     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = x.source)
448     JOIN vm ON (acvac.vis_attr_vector @@
449             (vm.c_attrs || $$&$$ ||
450                 search.calculate_visibility_attribute_test(
451                     $$circ_lib$$,
452                     (SELECT ARRAY_AGG(id) FROM actor.org_unit_descendants($4))
453                 )
454             )::query_int
455          )
456 ';
457         END IF;
458     ELSE
459         opac_visibility_join := '';
460     END IF;
461
462     -- The following determines whether we only provide suggestsons matching
463     -- the user's selected search_class, or whether we show other suggestions
464     -- too. The reason for MIN() is that for search_classes like
465     -- 'title|proper|uniform' you would otherwise get multiple rows.  The
466     -- implication is that if title as a class doesn't have restrict,
467     -- nor does the proper field, but the uniform field does, you're going
468     -- to get 'false' for your overall evaluation of 'should we restrict?'
469     -- To invert that, change from MIN() to MAX().
470
471     SELECT
472         INTO r_fields
473             MIN(cmc.restrict::INT) AS restrict_class,
474             MIN(cmf.restrict::INT) AS restrict_field
475         FROM metabib.search_class_to_registered_components(search_class)
476             AS _registered (field_class TEXT, field INT)
477         JOIN
478             config.metabib_class cmc ON (cmc.name = _registered.field_class)
479         LEFT JOIN
480             config.metabib_field cmf ON (cmf.id = _registered.field);
481
482     -- evaluate 'should we restrict?'
483     IF r_fields.restrict_field::BOOL OR r_fields.restrict_class::BOOL THEN
484         search_class_join := '
485     JOIN
486         metabib.search_class_to_registered_components($2)
487         AS _registered (field_class TEXT, field INT) ON (
488             (_registered.field IS NULL AND
489                 _registered.field_class = cmf.field_class) OR
490             (_registered.field = cmf.id)
491         )
492     ';
493     ELSE
494         search_class_join := '
495     LEFT JOIN
496         metabib.search_class_to_registered_components($2)
497         AS _registered (field_class TEXT, field INT) ON (
498             _registered.field_class = cmc.name
499         )
500     ';
501     END IF;
502
503     RETURN QUERY EXECUTE '
504 WITH vm AS ( SELECT * FROM asset.patron_default_visibility_mask() ),
505      mbe AS (SELECT * FROM metabib.browse_entry WHERE index_vector @@ $1 LIMIT 10000)
506 SELECT  DISTINCT
507         x.value,
508         x.id,
509         x.push,
510         x.restrict,
511         x.weight,
512         x.ts_rank_cd,
513         x.buoyant,
514         TS_HEADLINE(value, $7, $3)
515   FROM  (SELECT DISTINCT
516                 mbe.value,
517                 cmf.id,
518                 cmc.buoyant AND _registered.field_class IS NOT NULL AS push,
519                 _registered.field = cmf.id AS restrict,
520                 cmf.weight,
521                 TS_RANK_CD(mbe.index_vector, $1, $6),
522                 cmc.buoyant,
523                 mbedm.source
524           FROM  metabib.browse_entry_def_map mbedm
525                 JOIN mbe ON (mbe.id = mbedm.entry)
526                 JOIN config.metabib_field cmf ON (cmf.id = mbedm.def)
527                 JOIN config.metabib_class cmc ON (cmf.field_class = cmc.name)
528                 '  || search_class_join || '
529           ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
530           LIMIT 1000) AS x
531         ' || opac_visibility_join || '
532   ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
533   LIMIT $5
534 '   -- sic, repeat the order by clause in the outer select too
535     USING
536         query, search_class, headline_opts,
537         visibility_org, query_limit, normalization, plain_query
538         ;
539
540     -- sort order:
541     --  buoyant AND chosen class = match class
542     --  chosen field = match field
543     --  field weight
544     --  rank
545     --  buoyancy
546     --  value itself
547
548 END;
549 $f$ LANGUAGE plpgsql ROWS 10;
550
551 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)
552  RETURNS SETOF metabib.flat_browse_entry_appearance
553 AS $f$
554 DECLARE
555     core_query              TEXT;
556     back_query              TEXT;
557     forward_query           TEXT;
558     pivot_sort_value        TEXT;
559     pivot_sort_fallback     TEXT;
560     context_locations       INT[];
561     browse_superpage_size   INT;
562     results_skipped         INT := 0;
563     back_limit              INT;
564     back_to_pivot           INT;
565     forward_limit           INT;
566     forward_to_pivot        INT;
567 BEGIN
568     -- First, find the pivot if we were given a browse term but not a pivot.
569     IF pivot_id IS NULL THEN
570         pivot_id := metabib.browse_pivot(search_field, browse_term);
571     END IF;
572
573     SELECT INTO pivot_sort_value, pivot_sort_fallback
574         sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
575
576     -- Bail if we couldn't find a pivot.
577     IF pivot_sort_value IS NULL THEN
578         RETURN;
579     END IF;
580
581     -- Transform the context_loc_group argument (if any) (logc at the
582     -- TPAC layer) into a form we'll be able to use.
583     IF context_loc_group IS NOT NULL THEN
584         SELECT INTO context_locations ARRAY_AGG(location)
585             FROM asset.copy_location_group_map
586             WHERE lgroup = context_loc_group;
587     END IF;
588
589     -- Get the configured size of browse superpages.
590     SELECT INTO browse_superpage_size COALESCE(value::INT,100)     -- NULL ok
591         FROM config.global_flag
592         WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
593
594     -- First we're going to search backward from the pivot, then we're going
595     -- to search forward.  In each direction, we need two limits.  At the
596     -- lesser of the two limits, we delineate the edge of the result set
597     -- we're going to return.  At the greater of the two limits, we find the
598     -- pivot value that would represent an offset from the current pivot
599     -- at a distance of one "page" in either direction, where a "page" is a
600     -- result set of the size specified in the "result_limit" argument.
601     --
602     -- The two limits in each direction make four derived values in total,
603     -- and we calculate them now.
604     back_limit := CEIL(result_limit::FLOAT / 2);
605     back_to_pivot := result_limit;
606     forward_limit := result_limit / 2;
607     forward_to_pivot := result_limit - 1;
608
609     -- This is the meat of the SQL query that finds browse entries.  We'll
610     -- pass this to a function which uses it with a cursor, so that individual
611     -- rows may be fetched in a loop until some condition is satisfied, without
612     -- waiting for a result set of fixed size to be collected all at once.
613     core_query := '
614 SELECT  mbe.id,
615         mbe.value,
616         mbe.sort_value
617   FROM  metabib.browse_entry mbe
618   WHERE (
619             EXISTS ( -- are there any bibs using this mbe via the requested fields?
620                 SELECT  1
621                   FROM  metabib.browse_entry_def_map mbedm
622                   WHERE mbedm.entry = mbe.id AND mbedm.def = ANY(' || quote_literal(search_field) || ')
623             ) OR EXISTS ( -- are there any authorities using this mbe via the requested fields?
624                 SELECT  1
625                   FROM  metabib.browse_entry_simple_heading_map mbeshm
626                         JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
627                         JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
628                             ash.atag = map.authority_field
629                             AND map.metabib_field = ANY(' || quote_literal(search_field) || ')
630                         )
631                   WHERE mbeshm.entry = mbe.id
632             )
633         ) AND ';
634
635     -- This is the variant of the query for browsing backward.
636     back_query := core_query ||
637         ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
638     ' ORDER BY mbe.sort_value DESC, mbe.value DESC LIMIT 1000';
639
640     -- This variant browses forward.
641     forward_query := core_query ||
642         ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
643     ' ORDER BY mbe.sort_value, mbe.value LIMIT 1000';
644
645     -- We now call the function which applies a cursor to the provided
646     -- queries, stopping at the appropriate limits and also giving us
647     -- the next page's pivot.
648     RETURN QUERY
649         SELECT * FROM metabib.staged_browse(
650             back_query, search_field, context_org, context_locations,
651             staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
652         ) UNION
653         SELECT * FROM metabib.staged_browse(
654             forward_query, search_field, context_org, context_locations,
655             staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
656         ) ORDER BY row_number DESC;
657
658 END;
659 $f$ LANGUAGE plpgsql ROWS 10;
660
661 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)
662  RETURNS SETOF metabib.flat_browse_entry_appearance
663 AS $f$
664 DECLARE
665     curs                    REFCURSOR;
666     rec                     RECORD;
667     qpfts_query             TEXT;
668     aqpfts_query            TEXT;
669     afields                 INT[];
670     bfields                 INT[];
671     result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
672     results_skipped         INT := 0;
673     row_counter             INT := 0;
674     row_number              INT;
675     slice_start             INT;
676     slice_end               INT;
677     full_end                INT;
678     all_records             BIGINT[];
679     all_brecords             BIGINT[];
680     all_arecords            BIGINT[];
681     superpage_of_records    BIGINT[];
682     superpage_size          INT;
683     c_tests                 TEXT := '';
684     b_tests                 TEXT := '';
685     c_orgs                  INT[];
686 BEGIN
687     IF count_up_from_zero THEN
688         row_number := 0;
689     ELSE
690         row_number := -1;
691     END IF;
692
693     IF NOT staff THEN
694         SELECT x.c_attrs, x.b_attrs INTO c_tests, b_tests FROM asset.patron_default_visibility_mask() x;
695     END IF;
696
697     IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
698     IF b_tests <> '' THEN b_tests := b_tests || '&'; END IF;
699
700     SELECT ARRAY_AGG(id) INTO c_orgs FROM actor.org_unit_descendants(context_org);
701     
702     c_tests := c_tests || search.calculate_visibility_attribute_test('circ_lib',c_orgs)
703                || '&' || search.calculate_visibility_attribute_test('owning_lib',c_orgs);
704     
705     PERFORM 1 FROM config.internal_flag WHERE enabled AND name = 'opac.located_uri.act_as_copy';
706     IF FOUND THEN
707         b_tests := b_tests || search.calculate_visibility_attribute_test(
708             'luri_org',
709             (SELECT ARRAY_AGG(id) FROM actor.org_unit_full_path(context_org) x)
710         );
711     ELSE
712         b_tests := b_tests || search.calculate_visibility_attribute_test(
713             'luri_org',
714             (SELECT ARRAY_AGG(id) FROM actor.org_unit_ancestors(context_org) x)
715         );
716     END IF;
717
718     IF context_locations THEN
719         IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
720         c_tests := c_tests || search.calculate_visibility_attribute_test('location',context_locations);
721     END IF;
722
723     OPEN curs NO SCROLL FOR EXECUTE query;
724
725     LOOP
726         FETCH curs INTO rec;
727         IF NOT FOUND THEN
728             IF result_row.pivot_point IS NOT NULL THEN
729                 RETURN NEXT result_row;
730             END IF;
731             RETURN;
732         END IF;
733
734         -- Gather aggregate data based on the MBE row we're looking at now, authority axis
735         SELECT INTO all_arecords, result_row.sees, afields
736                 ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
737                 STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
738                 ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
739
740           FROM  metabib.browse_entry_simple_heading_map mbeshm
741                 JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
742                 JOIN authority.authority_linking aal ON ( ash.record = aal.source )
743                 JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
744                 JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
745                     ash.atag = map.authority_field
746                     AND map.metabib_field = ANY(fields)
747                 )
748           WHERE mbeshm.entry = rec.id;
749
750         -- Gather aggregate data based on the MBE row we're looking at now, bib axis
751         SELECT INTO all_brecords, result_row.authorities, bfields
752                 ARRAY_AGG(DISTINCT source),
753                 STRING_AGG(DISTINCT authority::TEXT, $$,$$),
754                 ARRAY_AGG(DISTINCT def)
755           FROM  metabib.browse_entry_def_map
756           WHERE entry = rec.id
757                 AND def = ANY(fields);
758
759         SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
760
761         result_row.sources := 0;
762         result_row.asources := 0;
763
764         -- Bib-linked vis checking
765         IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
766
767             SELECT  INTO result_row.sources COUNT(DISTINCT b.id)
768               FROM  biblio.record_entry b
769                     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
770               WHERE b.id = ANY(all_brecords[1:browse_superpage_size])
771                     AND (
772                         acvac.vis_attr_vector @@ c_tests::query_int
773                         OR b.vis_attr_vector @@ b_tests::query_int
774                     );
775
776             result_row.accurate := TRUE;
777
778         END IF;
779
780         -- Authority-linked vis checking
781         IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
782
783             SELECT  INTO result_row.asources COUNT(DISTINCT b.id)
784               FROM  biblio.record_entry b
785                     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
786               WHERE b.id = ANY(all_arecords[1:browse_superpage_size])
787                     AND (
788                         acvac.vis_attr_vector @@ c_tests::query_int
789                         OR b.vis_attr_vector @@ b_tests::query_int
790                     );
791
792             result_row.aaccurate := TRUE;
793
794         END IF;
795
796         IF result_row.sources > 0 OR result_row.asources > 0 THEN
797
798             -- The function that calls this function needs row_number in order
799             -- to correctly order results from two different runs of this
800             -- functions.
801             result_row.row_number := row_number;
802
803             -- Now, if row_counter is still less than limit, return a row.  If
804             -- not, but it is less than next_pivot_pos, continue on without
805             -- returning actual result rows until we find
806             -- that next pivot, and return it.
807
808             IF row_counter < result_limit THEN
809                 result_row.browse_entry := rec.id;
810                 result_row.value := rec.value;
811
812                 RETURN NEXT result_row;
813             ELSE
814                 result_row.browse_entry := NULL;
815                 result_row.authorities := NULL;
816                 result_row.fields := NULL;
817                 result_row.value := NULL;
818                 result_row.sources := NULL;
819                 result_row.sees := NULL;
820                 result_row.accurate := NULL;
821                 result_row.aaccurate := NULL;
822                 result_row.pivot_point := rec.id;
823
824                 IF row_counter >= next_pivot_pos THEN
825                     RETURN NEXT result_row;
826                     RETURN;
827                 END IF;
828             END IF;
829
830             IF count_up_from_zero THEN
831                 row_number := row_number + 1;
832             ELSE
833                 row_number := row_number - 1;
834             END IF;
835
836             -- row_counter is different from row_number.
837             -- It simply counts up from zero so that we know when
838             -- we've reached our limit.
839             row_counter := row_counter + 1;
840         END IF;
841     END LOOP;
842 END;
843 $f$ LANGUAGE plpgsql ROWS 10;
844
845 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON biblio.peer_bib_copy_map;
846 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON biblio.record_entry;
847 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.copy;
848 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.call_number;
849 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.copy_location;
850 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON serial.unit;
851 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON config.copy_status;
852 DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON actor.org_unit;
853
854 -- Upgrade the data!
855 INSERT INTO asset.copy_vis_attr_cache (target_copy, record, vis_attr_vector)
856     SELECT  cp.id,
857             cn.record,
858             asset.calculate_copy_visibility_attribute_set(cp.id)
859       FROM  asset.copy cp
860             JOIN asset.call_number cn ON (cp.call_number = cn.id);
861
862 UPDATE biblio.record_entry SET vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(id);
863
864 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();
865 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();
866 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER UPDATE ON asset.call_number FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
867 CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
868 CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
869 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
870 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
871
872 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$
873 DECLARE
874     ans RECORD;
875     trans INT;
876 BEGIN
877     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;
878
879     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
880         RETURN QUERY
881         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
882              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
883              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
884         SELECT  ans.depth,
885                 ans.id,
886                 COUNT( av.id ),
887                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
888                 COUNT( av.id ),
889                 trans
890           FROM  mask,
891                 available_statuses,
892                 org_list,
893                 asset.copy_vis_attr_cache av
894                 JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
895           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
896           GROUP BY 1,2,6;
897
898         IF NOT FOUND THEN
899             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
900         END IF;
901
902     END LOOP;
903
904     RETURN;
905 END;
906 $f$ LANGUAGE PLPGSQL;
907
908 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$
909 DECLARE
910     ans RECORD;
911     trans INT;
912 BEGIN
913     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;
914
915     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
916         RETURN QUERY
917         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
918              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
919              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
920         SELECT  -1,
921                 ans.id,
922                 COUNT( av.id ),
923                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
924                 COUNT( av.id ),
925                 trans
926           FROM  mask,
927                 org_list,
928                 asset.copy_vis_attr_cache av
929                 JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
930           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
931           GROUP BY 1,2,6;
932
933         IF NOT FOUND THEN
934             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
935         END IF;
936
937     END LOOP;
938
939     RETURN;
940 END;
941 $f$ LANGUAGE PLPGSQL;
942
943 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$
944 DECLARE
945     ans RECORD;
946     trans INT;
947 BEGIN
948     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;
949
950     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
951         RETURN QUERY
952         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
953              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
954              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
955         SELECT  ans.depth,
956                 ans.id,
957                 COUNT( av.id ),
958                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
959                 COUNT( av.id ),
960                 trans
961           FROM  mask,
962                 org_list,
963                 available_statuses,
964                 asset.copy_vis_attr_cache av
965                 JOIN asset.copy cp ON (cp.id = av.target_copy)
966                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
967           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
968           GROUP BY 1,2,6;
969
970         IF NOT FOUND THEN
971             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
972         END IF;
973
974     END LOOP;
975
976     RETURN;
977 END;
978 $f$ LANGUAGE PLPGSQL;
979
980 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$
981 DECLARE
982     ans RECORD;
983     trans INT;
984 BEGIN
985     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;
986
987     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
988         RETURN QUERY
989         WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
990              available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
991              mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
992         SELECT  -1,
993                 ans.id,
994                 COUNT( av.id ),
995                 SUM( (cp.status = ANY (available_statuses.ids))::INT ),
996                 COUNT( av.id ),
997                 trans
998           FROM  mask,
999                 org_list,
1000                 available_statuses,
1001                 asset.copy_vis_attr_cache av
1002                 JOIN asset.copy cp ON (cp.id = av.target_copy)
1003                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
1004           WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
1005           GROUP BY 1,2,6;
1006
1007         IF NOT FOUND THEN
1008             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1009         END IF;
1010
1011     END LOOP;
1012
1013     RETURN;
1014 END;
1015 $f$ LANGUAGE PLPGSQL;
1016
1017 CREATE OR REPLACE FUNCTION unapi.mmr_mra (
1018     obj_id BIGINT,
1019     format TEXT,
1020     ename TEXT,
1021     includes TEXT[],
1022     org TEXT,
1023     depth INT DEFAULT NULL,
1024     slimit HSTORE DEFAULT NULL,
1025     soffset HSTORE DEFAULT NULL,
1026     include_xmlns BOOL DEFAULT TRUE,
1027     pref_lib INT DEFAULT NULL
1028 ) RETURNS XML AS $F$
1029     SELECT  XMLELEMENT(
1030         name attributes,
1031         XMLATTRIBUTES(
1032             CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
1033             'tag:open-ils.org:U2@mmr/' || $1 AS metarecord
1034         ),
1035         (SELECT XMLAGG(foo.y)
1036           FROM (
1037             WITH sourcelist AS (
1038                 WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id FROM actor.org_unit WHERE shortname = $5 LIMIT 1),
1039                      basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
1040                      circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY_AGG(aoud.id)) AS mask
1041                                   FROM aou, LATERAL actor.org_unit_descendants(aou.id, $6) aoud)
1042                 SELECT  source
1043                   FROM  aou, circvm, basevm, metabib.metarecord_source_map mmsm
1044                   WHERE mmsm.metarecord = $1 AND (
1045                     EXISTS (
1046                         SELECT  1
1047                           FROM  circvm, basevm, asset.copy_vis_attr_cache acvac
1048                           WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
1049                                 AND acvac.record = mmsm.source
1050                     )
1051                     OR EXISTS (SELECT 1 FROM evergreen.located_uris(source, aou.id, $10) LIMIT 1)
1052                     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)
1053                 )
1054             )
1055             SELECT  cmra.aid,
1056                     XMLELEMENT(
1057                         name field,
1058                         XMLATTRIBUTES(
1059                             cmra.attr AS name,
1060                             cmra.value AS "coded-value",
1061                             cmra.aid AS "cvmid",
1062                             rad.composite,
1063                             rad.multi,
1064                             rad.filter,
1065                             rad.sorter,
1066                             cmra.source_list
1067                         ),
1068                         cmra.value
1069                     )
1070               FROM  (
1071                 SELECT DISTINCT aid, attr, value, STRING_AGG(x.id::TEXT, ',') AS source_list
1072                   FROM (
1073                     SELECT  v.source AS id,
1074                             c.id AS aid,
1075                             c.ctype AS attr,
1076                             c.code AS value
1077                       FROM  metabib.record_attr_vector_list v
1078                             JOIN config.coded_value_map c ON ( c.id = ANY( v.vlist ) )
1079                     ) AS x
1080                     JOIN sourcelist ON (x.id = sourcelist.source)
1081                     GROUP BY 1, 2, 3
1082                 ) AS cmra
1083                 JOIN config.record_attr_definition rad ON (cmra.attr = rad.name)
1084                 UNION ALL
1085             SELECT  umra.aid,
1086                     XMLELEMENT(
1087                         name field,
1088                         XMLATTRIBUTES(
1089                             umra.attr AS name,
1090                             rad.composite,
1091                             rad.multi,
1092                             rad.filter,
1093                             rad.sorter
1094                         ),
1095                         umra.value
1096                     )
1097               FROM  (
1098                 SELECT DISTINCT aid, attr, value
1099                   FROM (
1100                     SELECT  v.source AS id,
1101                             m.id AS aid,
1102                             m.attr AS attr,
1103                             m.value AS value
1104                       FROM  metabib.record_attr_vector_list v
1105                             JOIN metabib.uncontrolled_record_attr_value m ON ( m.id = ANY( v.vlist ) )
1106                     ) AS x
1107                     JOIN sourcelist ON (x.id = sourcelist.source)
1108                 ) AS umra
1109                 JOIN config.record_attr_definition rad ON (umra.attr = rad.name)
1110                 ORDER BY 1
1111
1112             )foo(id,y)
1113         )
1114     )
1115 $F$ LANGUAGE SQL STABLE;
1116
1117 CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
1118     bibid BIGINT[],
1119     ouid INT,
1120     depth INT DEFAULT NULL,
1121     slimit HSTORE DEFAULT NULL,
1122     soffset HSTORE DEFAULT NULL,
1123     pref_lib INT DEFAULT NULL,
1124     includes TEXT[] DEFAULT NULL::TEXT[]
1125 ) RETURNS TABLE(id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT) AS $$
1126     WITH RECURSIVE ou_depth AS (
1127         SELECT COALESCE(
1128             $3,
1129             (
1130                 SELECT depth
1131                 FROM actor.org_unit_type aout
1132                     INNER JOIN actor.org_unit ou ON ou_type = aout.id
1133                 WHERE ou.id = $2
1134             )
1135         ) AS depth
1136     ), descendant_depth AS (
1137         SELECT  ou.id,
1138                 ou.parent_ou,
1139                 out.depth
1140         FROM  actor.org_unit ou
1141                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
1142                 JOIN anscestor_depth ad ON (ad.id = ou.id),
1143                 ou_depth
1144         WHERE ad.depth = ou_depth.depth
1145             UNION ALL
1146         SELECT  ou.id,
1147                 ou.parent_ou,
1148                 out.depth
1149         FROM  actor.org_unit ou
1150                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
1151                 JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
1152     ), anscestor_depth AS (
1153         SELECT  ou.id,
1154                 ou.parent_ou,
1155                 out.depth
1156         FROM  actor.org_unit ou
1157                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
1158         WHERE ou.id = $2
1159             UNION ALL
1160         SELECT  ou.id,
1161                 ou.parent_ou,
1162                 out.depth
1163         FROM  actor.org_unit ou
1164                 JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
1165                 JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
1166     ), descendants as (
1167         SELECT ou.* FROM actor.org_unit ou JOIN descendant_depth USING (id)
1168     )
1169
1170     SELECT ua.id, ua.name, ua.label_sortkey, MIN(ua.rank) AS rank FROM (
1171         SELECT acn.id, owning_lib.name, acn.label_sortkey,
1172             evergreen.rank_cp(acp),
1173             RANK() OVER w
1174         FROM asset.call_number acn
1175             JOIN asset.copy acp ON (acn.id = acp.call_number)
1176             JOIN descendants AS aou ON (acp.circ_lib = aou.id)
1177             JOIN actor.org_unit AS owning_lib ON (acn.owning_lib = owning_lib.id)
1178         WHERE acn.record = ANY ($1)
1179             AND acn.deleted IS FALSE
1180             AND acp.deleted IS FALSE
1181             AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN
1182                 EXISTS (
1183                     WITH basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
1184                          circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY[acp.circ_lib]) AS mask)
1185                     SELECT  1
1186                       FROM  basevm, circvm, asset.copy_vis_attr_cache acvac
1187                       WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
1188                             AND acvac.target_copy = acp.id
1189                             AND acvac.record = acn.record
1190                 ) ELSE TRUE END
1191         GROUP BY acn.id, evergreen.rank_cp(acp), owning_lib.name, acn.label_sortkey, aou.id
1192         WINDOW w AS (
1193             ORDER BY
1194                 COALESCE(
1195                     CASE WHEN aou.id = $2 THEN -20000 END,
1196                     CASE WHEN aou.id = $6 THEN -10000 END,
1197                     (SELECT distance - 5000
1198                         FROM actor.org_unit_descendants_distance($6) as x
1199                         WHERE x.id = aou.id AND $6 IN (
1200                             SELECT q.id FROM actor.org_unit_descendants($2) as q)),
1201                     (SELECT e.distance FROM actor.org_unit_descendants_distance($2) as e WHERE e.id = aou.id),
1202                     1000
1203                 ),
1204                 evergreen.rank_cp(acp)
1205         )
1206     ) AS ua
1207     GROUP BY ua.id, ua.name, ua.label_sortkey
1208     ORDER BY rank, ua.name, ua.label_sortkey
1209     LIMIT ($4 -> 'acn')::INT
1210     OFFSET ($5 -> 'acn')::INT;
1211 $$ LANGUAGE SQL STABLE ROWS 10;
1212
1213 COMMIT;
1214