]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/upgrade/0983.schema.statistical-ratings.sql
LP#1842940: Improve Alert
[Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / 0983.schema.statistical-ratings.sql
1
2 BEGIN;
3
4 SELECT evergreen.upgrade_deps_block_check('0983', :eg_version);
5
6 -- Create these so that the queries in the UDFs will validate
7 CREATE TEMP TABLE precalc_filter_bib_list (
8     id  BIGINT
9 ) ON COMMIT DROP;
10
11 CREATE TEMP TABLE precalc_bib_filter_bib_list (
12     id  BIGINT
13 ) ON COMMIT DROP;
14
15 CREATE TEMP TABLE precalc_src_filter_bib_list (
16     id  BIGINT
17 ) ON COMMIT DROP;
18
19 CREATE TEMP TABLE precalc_copy_filter_bib_list (
20     id  BIGINT,
21     copy  BIGINT
22 ) ON COMMIT DROP;
23
24 CREATE TEMP TABLE precalc_circ_mod_filter_bib_list (
25     id  BIGINT,
26     copy  BIGINT
27 ) ON COMMIT DROP;
28
29 CREATE TEMP TABLE precalc_location_filter_bib_list (
30     id  BIGINT,
31     copy  BIGINT
32 ) ON COMMIT DROP;
33
34 CREATE TEMP TABLE precalc_attr_filter_bib_list (
35     id  BIGINT
36 ) ON COMMIT DROP;
37
38 CREATE TEMP TABLE precalc_bibs_by_copy_list (
39     id  BIGINT
40 ) ON COMMIT DROP;
41
42 CREATE TEMP TABLE precalc_bibs_by_uri_list (
43     id  BIGINT
44 ) ON COMMIT DROP;
45
46 CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list (
47     id  BIGINT
48 ) ON COMMIT DROP;
49
50 CREATE TEMP TABLE precalc_bib_list (
51     id  BIGINT
52 ) ON COMMIT DROP;
53
54 -- rhrr needs to be a real table, so it can be fast. To that end, we use
55 -- a materialized view updated via a trigger.
56
57 DROP VIEW reporter.hold_request_record;
58
59 CREATE TABLE reporter.hold_request_record  AS
60 SELECT  id,
61         target,
62         hold_type,
63         CASE
64                 WHEN hold_type = 'T'
65                         THEN target
66                 WHEN hold_type = 'I'
67                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
68                 WHEN hold_type = 'V'
69                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
70                 WHEN hold_type IN ('C','R','F')
71                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
72                 WHEN hold_type = 'M'
73                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
74                 WHEN hold_type = 'P'
75                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
76         END AS bib_record
77   FROM  action.hold_request ahr;
78
79 CREATE UNIQUE INDEX reporter_hold_request_record_pkey_idx ON reporter.hold_request_record (id);
80 CREATE INDEX reporter_hold_request_record_bib_record_idx ON reporter.hold_request_record (bib_record);
81
82 ALTER TABLE reporter.hold_request_record ADD PRIMARY KEY USING INDEX reporter_hold_request_record_pkey_idx;
83
84 CREATE FUNCTION reporter.hold_request_record_mapper () RETURNS TRIGGER AS $$
85 BEGIN
86     IF TG_OP = 'INSERT' THEN
87         INSERT INTO reporter.hold_request_record (id, target, hold_type, bib_record)
88         SELECT  NEW.id,
89                 NEW.target,
90                 NEW.hold_type,
91                 CASE
92                     WHEN NEW.hold_type = 'T'
93                         THEN NEW.target
94                     WHEN NEW.hold_type = 'I'
95                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
96                     WHEN NEW.hold_type = 'V'
97                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
98                     WHEN NEW.hold_type IN ('C','R','F')
99                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
100                     WHEN NEW.hold_type = 'M'
101                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
102                     WHEN NEW.hold_type = 'P'
103                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
104                 END AS bib_record;
105     ELSIF TG_OP = 'UPDATE' AND (OLD.target <> NEW.target OR OLD.hold_type <> NEW.hold_type) THEN
106         UPDATE  reporter.hold_request_record
107           SET   target = NEW.target,
108                 hold_type = NEW.hold_type,
109                 bib_record = CASE
110                     WHEN NEW.hold_type = 'T'
111                         THEN NEW.target
112                     WHEN NEW.hold_type = 'I'
113                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
114                     WHEN NEW.hold_type = 'V'
115                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
116                     WHEN NEW.hold_type IN ('C','R','F')
117                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
118                     WHEN NEW.hold_type = 'M'
119                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
120                     WHEN NEW.hold_type = 'P'
121                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
122                 END;
123     END IF;
124     RETURN NEW;
125 END;
126 $$ LANGUAGE PLPGSQL;
127
128 CREATE TRIGGER reporter_hold_request_record_trigger AFTER INSERT OR UPDATE ON action.hold_request
129     FOR EACH ROW EXECUTE PROCEDURE reporter.hold_request_record_mapper();
130
131 CREATE SCHEMA rating;
132
133 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
134     'opac.default_sort',
135     'OPAC Default Sort (titlesort, authorsort, pubdate, popularity, poprel, or empty for relevance)',
136     '',
137     TRUE
138 );
139
140 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
141     'search.max_popularity_importance_multiplier',
142     oils_i18n_gettext(
143         'search.max_popularity_importance_multiplier',
144         'Maximum popularity importance multiplier for popularity-adjusted relevance searches (decimal value between 1.0 and 2.0)',
145         'cgf',
146         'label'
147     ),
148     '1.1',
149     TRUE
150 );
151
152 CREATE TABLE rating.popularity_parameter (
153     id          INT     PRIMARY KEY,
154     name        TEXT    NOT NULL UNIQUE, -- i18n
155     description TEXT,
156     func        TEXT,
157     require_horizon     BOOL    NOT NULL DEFAULT FALSE,
158     require_importance  BOOL    NOT NULL DEFAULT FALSE,
159     require_percentile  BOOL    NOT NULL DEFAULT FALSE
160 );
161
162 INSERT INTO rating.popularity_parameter (id,name,func,require_horizon,require_importance,require_percentile) VALUES
163     (1,'Holds Filled Over Time','rating.holds_filled_over_time',TRUE,FALSE,TRUE),
164     (2,'Holds Requested Over Time','rating.holds_placed_over_time',TRUE,FALSE,TRUE),
165     (3,'Current Hold Count','rating.current_hold_count',FALSE,FALSE,TRUE),
166     (4,'Circulations Over Time','rating.circs_over_time',TRUE,FALSE,TRUE),
167     (5,'Current Circulation Count','rating.current_circ_count',FALSE,FALSE,TRUE),
168     (6,'Out/Total Ratio','rating.checked_out_total_ratio',FALSE,FALSE,TRUE),
169     (7,'Holds/Total Ratio','rating.holds_total_ratio',FALSE,FALSE,TRUE),
170     (8,'Holds/Holdable Ratio','rating.holds_holdable_ratio',FALSE,FALSE,TRUE),
171     (9,'Percent of Time Circulating','rating.percent_time_circulating',FALSE,FALSE,TRUE),
172     (10,'Bibliographic Record Age (days, newer is better)','rating.bib_record_age',FALSE,FALSE,TRUE),
173     (11,'Publication Age (days, newer is better)','rating.bib_pub_age',FALSE,FALSE,TRUE),
174     (12,'On-line Bib has attributes','rating.generic_fixed_rating_by_uri',FALSE,FALSE,FALSE),
175     (13,'Bib has attributes and copies','rating.generic_fixed_rating_by_copy',FALSE,FALSE,FALSE),
176     (14,'Bib has attributes and copies or URIs','rating.generic_fixed_rating_by_copy_or_uri',FALSE,FALSE,FALSE),
177     (15,'Bib has attributes','rating.generic_fixed_rating_global',FALSE,FALSE,FALSE);
178
179 CREATE TABLE rating.badge (
180     id                      SERIAL      PRIMARY KEY,
181     name                    TEXT        NOT NULL,
182     description             TEXT,
183     scope                   INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
184     weight                  INT         NOT NULL DEFAULT 1,
185     horizon_age             INTERVAL,
186     importance_age          INTERVAL,
187     importance_interval     INTERVAL    NOT NULL DEFAULT '1 day',
188     importance_scale        NUMERIC     CHECK (importance_scale IS NULL OR importance_scale > 0.0),
189     recalc_interval         INTERVAL    NOT NULL DEFAULT '1 month',
190     attr_filter             TEXT,
191     src_filter              INT         REFERENCES config.bib_source (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
192     circ_mod_filter         TEXT        REFERENCES config.circ_modifier (code) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
193     loc_grp_filter          INT         REFERENCES asset.copy_location_group (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
194     popularity_parameter    INT         NOT NULL REFERENCES rating.popularity_parameter (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
195     fixed_rating            INT         CHECK (fixed_rating IS NULL OR fixed_rating BETWEEN -5 AND 5),
196     percentile              NUMERIC     CHECK (percentile IS NULL OR (percentile >= 50.0 AND percentile < 100.0)),
197     discard                 INT         NOT NULL DEFAULT 0, 
198     last_calc               TIMESTAMPTZ,
199     CONSTRAINT unique_name_scope UNIQUE (name,scope)
200 );
201
202 CREATE TABLE rating.record_badge_score (
203     id          BIGSERIAL   PRIMARY KEY,
204     record      BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
205     badge       INT         NOT NULL REFERENCES rating.badge (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
206     score       INT         NOT NULL CHECK (score BETWEEN -5 AND 5),
207     CONSTRAINT unique_record_badge UNIQUE (record,badge)
208 );
209 CREATE INDEX record_badge_score_badge_idx ON rating.record_badge_score (badge);
210 CREATE INDEX record_badge_score_record_idx ON rating.record_badge_score (record);
211
212 CREATE OR REPLACE VIEW rating.badge_with_orgs AS
213     WITH    org_scope AS (
214                 SELECT  id,
215                         array_agg(tree) AS orgs
216                   FROM  (SELECT id,
217                                 (actor.org_unit_descendants(id)).id AS tree
218                           FROM  actor.org_unit
219                         ) x
220                   GROUP BY 1
221             )
222     SELECT  b.*,
223             s.orgs
224       FROM  rating.badge b
225             JOIN org_scope s ON (b.scope = s.id);
226
227 CREATE OR REPLACE FUNCTION rating.precalc_src_filter(src INT)
228     RETURNS INT AS $f$
229 DECLARE
230     cnt     INT     := 0;
231 BEGIN
232
233     SET LOCAL client_min_messages = error;
234     DROP TABLE IF EXISTS precalc_src_filter_bib_list;
235     IF src IS NOT NULL THEN
236         CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
237             SELECT id FROM biblio.record_entry
238             WHERE source = src AND NOT deleted;
239     ELSE
240         CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
241             SELECT id FROM biblio.record_entry
242             WHERE id > 0 AND NOT deleted;
243     END IF;
244
245     SELECT count(*) INTO cnt FROM precalc_src_filter_bib_list;
246     RETURN cnt;
247 END;
248 $f$ LANGUAGE PLPGSQL;
249
250 CREATE OR REPLACE FUNCTION rating.precalc_circ_mod_filter(cm TEXT)
251     RETURNS INT AS $f$
252 DECLARE
253     cnt     INT     := 0;
254 BEGIN
255
256     SET LOCAL client_min_messages = error;
257     DROP TABLE IF EXISTS precalc_circ_mod_filter_bib_list;
258     IF cm IS NOT NULL THEN
259         CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
260             SELECT  cn.record AS id,
261                     cp.id AS copy
262               FROM  asset.call_number cn
263                     JOIN asset.copy cp ON (cn.id = cp.call_number)
264               WHERE cp.circ_modifier = cm
265                     AND NOT cp.deleted;
266     ELSE
267         CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
268             SELECT  cn.record AS id,
269                     cp.id AS copy
270               FROM  asset.call_number cn
271                     JOIN asset.copy cp ON (cn.id = cp.call_number)
272               WHERE NOT cp.deleted;
273     END IF;
274
275     SELECT count(*) INTO cnt FROM precalc_circ_mod_filter_bib_list;
276     RETURN cnt;
277 END;
278 $f$ LANGUAGE PLPGSQL;
279
280 CREATE OR REPLACE FUNCTION rating.precalc_location_filter(loc INT)
281     RETURNS INT AS $f$
282 DECLARE
283     cnt     INT     := 0;
284 BEGIN
285
286     SET LOCAL client_min_messages = error;
287     DROP TABLE IF EXISTS precalc_location_filter_bib_list;
288     IF loc IS NOT NULL THEN
289         CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
290             SELECT  cn.record AS id,
291                     cp.id AS copy
292               FROM  asset.call_number cn
293                     JOIN asset.copy cp ON (cn.id = cp.call_number)
294                     JOIN asset.copy_location_group_map lg ON (cp.location = lg.location)
295               WHERE lg.lgroup = loc
296                     AND NOT cp.deleted;
297     ELSE
298         CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
299             SELECT  cn.record AS id,
300                     cp.id AS copy
301               FROM  asset.call_number cn
302                     JOIN asset.copy cp ON (cn.id = cp.call_number)
303               WHERE NOT cp.deleted;
304     END IF;
305
306     SELECT count(*) INTO cnt FROM precalc_location_filter_bib_list;
307     RETURN cnt;
308 END;
309 $f$ LANGUAGE PLPGSQL;
310
311 -- all or limited...
312 CREATE OR REPLACE FUNCTION rating.precalc_attr_filter(attr_filter TEXT)
313     RETURNS INT AS $f$
314 DECLARE
315     cnt     INT := 0;
316     afilter TEXT;
317 BEGIN
318
319     SET LOCAL client_min_messages = error;
320     DROP TABLE IF EXISTS precalc_attr_filter_bib_list;
321     IF attr_filter IS NOT NULL THEN
322         afilter := metabib.compile_composite_attr(attr_filter);
323         CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
324             SELECT source AS id FROM metabib.record_attr_vector_list
325             WHERE vlist @@ metabib.compile_composite_attr(attr_filter);
326     ELSE
327         CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
328             SELECT source AS id FROM metabib.record_attr_vector_list;
329     END IF;
330
331     SELECT count(*) INTO cnt FROM precalc_attr_filter_bib_list;
332     RETURN cnt;
333 END;
334 $f$ LANGUAGE PLPGSQL;
335
336 CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy(badge_id INT)
337     RETURNS INT AS $f$
338 DECLARE
339     cnt         INT     := 0;
340     badge_row   rating.badge_with_orgs%ROWTYPE;
341     base        TEXT;
342     whr         TEXT;
343 BEGIN
344
345     SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
346
347     SET LOCAL client_min_messages = error;
348     DROP TABLE IF EXISTS precalc_bibs_by_copy_list;
349     CREATE TEMP TABLE precalc_bibs_by_copy_list ON COMMIT DROP AS
350         SELECT  DISTINCT cn.record AS id
351           FROM  asset.call_number cn
352                 JOIN asset.copy cp ON (cp.call_number = cn.id AND NOT cp.deleted)
353                 JOIN precalc_copy_filter_bib_list f ON (cp.id = f.copy)
354           WHERE cn.owning_lib = ANY (badge_row.orgs)
355                 AND NOT cn.deleted;
356
357     SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_list;
358     RETURN cnt;
359 END;
360 $f$ LANGUAGE PLPGSQL;
361
362 CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_uri(badge_id INT)
363     RETURNS INT AS $f$
364 DECLARE
365     cnt         INT     := 0;
366     badge_row   rating.badge_with_orgs%ROWTYPE;
367 BEGIN
368
369     SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
370
371     SET LOCAL client_min_messages = error;
372     DROP TABLE IF EXISTS precalc_bibs_by_uri_list;
373     CREATE TEMP TABLE precalc_bibs_by_uri_list ON COMMIT DROP AS
374         SELECT  DISTINCT record AS id
375           FROM  asset.call_number cn
376                 JOIN asset.uri_call_number_map urim ON (urim.call_number = cn.id)
377                 JOIN asset.uri uri ON (urim.uri = uri.id AND uri.active)
378           WHERE cn.owning_lib = ANY (badge_row.orgs)
379                 AND cn.label = '##URI##'
380                 AND NOT cn.deleted;
381
382     SELECT count(*) INTO cnt FROM precalc_bibs_by_uri_list;
383     RETURN cnt;
384 END;
385 $f$ LANGUAGE PLPGSQL;
386
387 CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy_or_uri(badge_id INT)
388     RETURNS INT AS $f$
389 DECLARE
390     cnt         INT     := 0;
391 BEGIN
392
393     PERFORM rating.precalc_bibs_by_copy(badge_id);
394     PERFORM rating.precalc_bibs_by_uri(badge_id);
395
396     SET LOCAL client_min_messages = error;
397     DROP TABLE IF EXISTS precalc_bibs_by_copy_or_uri_list;
398     CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list ON COMMIT DROP AS
399         SELECT id FROM precalc_bibs_by_copy_list
400             UNION
401         SELECT id FROM precalc_bibs_by_uri_list;
402
403     SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_or_uri_list;
404     RETURN cnt;
405 END;
406 $f$ LANGUAGE PLPGSQL;
407
408
409 CREATE OR REPLACE FUNCTION rating.recalculate_badge_score ( badge_id INT, setup_only BOOL DEFAULT FALSE ) RETURNS VOID AS $f$
410 DECLARE
411     badge_row           rating.badge%ROWTYPE;
412     param           rating.popularity_parameter%ROWTYPE;
413 BEGIN
414     SET LOCAL client_min_messages = error;
415
416     -- Find what we're doing    
417     SELECT * INTO badge_row FROM rating.badge WHERE id = badge_id;
418     SELECT * INTO param FROM rating.popularity_parameter WHERE id = badge_row.popularity_parameter;
419
420     -- Calculate the filtered bib set, or all bibs if none
421     PERFORM rating.precalc_attr_filter(badge_row.attr_filter);
422     PERFORM rating.precalc_src_filter(badge_row.src_filter);
423     PERFORM rating.precalc_circ_mod_filter(badge_row.circ_mod_filter);
424     PERFORM rating.precalc_location_filter(badge_row.loc_grp_filter);
425
426     -- Bring the bib-level filter lists together
427     DROP TABLE IF EXISTS precalc_bib_filter_bib_list;
428     CREATE TEMP TABLE precalc_bib_filter_bib_list ON COMMIT DROP AS
429         SELECT id FROM precalc_attr_filter_bib_list
430             INTERSECT
431         SELECT id FROM precalc_src_filter_bib_list;
432
433     -- Bring the copy-level filter lists together. We're keeping this for bib_by_copy filtering later.
434     DROP TABLE IF EXISTS precalc_copy_filter_bib_list;
435     CREATE TEMP TABLE precalc_copy_filter_bib_list ON COMMIT DROP AS
436         SELECT id, copy FROM precalc_circ_mod_filter_bib_list
437             INTERSECT
438         SELECT id, copy FROM precalc_location_filter_bib_list;
439
440     -- Bring the collapsed filter lists together
441     DROP TABLE IF EXISTS precalc_filter_bib_list;
442     CREATE TEMP TABLE precalc_filter_bib_list ON COMMIT DROP AS
443         SELECT id FROM precalc_bib_filter_bib_list
444             INTERSECT
445         SELECT id FROM precalc_copy_filter_bib_list;
446
447     CREATE INDEX precalc_filter_bib_list_idx
448         ON precalc_filter_bib_list (id);
449
450     IF setup_only THEN
451         RETURN;
452     END IF;
453
454     -- If it's a fixed-rating badge, just do it ...
455     IF badge_row.fixed_rating IS NOT NULL THEN
456         DELETE FROM rating.record_badge_score WHERE badge = badge_id;
457         EXECUTE $e$
458             INSERT INTO rating.record_badge_score (record, badge, score)
459                 SELECT record, $1, $2 FROM $e$ || param.func || $e$($1)$e$
460         USING badge_id, badge_row.fixed_rating;
461
462         UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
463
464         RETURN;
465     END IF;
466     -- else, calculate!
467
468     -- Make a session-local scratchpad for calculating scores
469     CREATE TEMP TABLE record_score_scratchpad (
470         bib     BIGINT,
471         value   NUMERIC
472     ) ON COMMIT DROP;
473
474     -- Gather raw values
475     EXECUTE $e$
476         INSERT INTO record_score_scratchpad (bib, value)
477             SELECT * FROM $e$ || param.func || $e$($1)$e$
478     USING badge_id;
479
480     IF badge_row.discard > 0 OR badge_row.percentile IS NOT NULL THEN
481         -- To speed up discard-common
482         CREATE INDEX record_score_scratchpad_score_idx ON record_score_scratchpad (value);
483         ANALYZE record_score_scratchpad;
484     END IF;
485
486     IF badge_row.discard > 0 THEN -- Remove common low values (trim the long tail)
487         DELETE FROM record_score_scratchpad WHERE value IN (
488             SELECT DISTINCT value FROM record_score_scratchpad ORDER BY value LIMIT badge_row.discard
489         );
490     END IF;
491
492     IF badge_row.percentile IS NOT NULL THEN -- Cut population down to exceptional records
493         DELETE FROM record_score_scratchpad WHERE value <= (
494             SELECT value FROM (
495                 SELECT  value,
496                         CUME_DIST() OVER (ORDER BY value) AS p
497                   FROM  record_score_scratchpad
498             ) x WHERE p < badge_row.percentile / 100.0 ORDER BY p DESC LIMIT 1
499         );
500     END IF;
501
502
503     -- And, finally, push new data in
504     DELETE FROM rating.record_badge_score WHERE badge = badge_id;
505     INSERT INTO rating.record_badge_score (badge, record, score)
506         SELECT  badge_id,
507                 bib,
508                 GREATEST(ROUND((CUME_DIST() OVER (ORDER BY value)) * 5), 1) AS value
509           FROM  record_score_scratchpad;
510
511     DROP TABLE record_score_scratchpad;
512
513     -- Now, finally-finally, mark the badge as recalculated
514     UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
515
516     RETURN;
517 END;
518 $f$ LANGUAGE PLPGSQL;
519
520 CREATE OR REPLACE FUNCTION rating.holds_filled_over_time(badge_id INT)
521     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
522 DECLARE
523     badge   rating.badge_with_orgs%ROWTYPE;
524     iage    INT     := 1;
525     iint    INT     := NULL;
526     iscale  NUMERIC := NULL;
527 BEGIN
528
529     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
530
531     IF badge.horizon_age IS NULL THEN
532         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
533             badge.name,
534             badge.id;
535     END IF;
536
537     PERFORM rating.precalc_bibs_by_copy(badge_id);
538
539     SET LOCAL client_min_messages = error;
540     DROP TABLE IF EXISTS precalc_bib_list;
541     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
542         SELECT id FROM precalc_filter_bib_list
543             INTERSECT
544         SELECT id FROM precalc_bibs_by_copy_list;
545
546     iint := EXTRACT(EPOCH FROM badge.importance_interval);
547     IF badge.importance_age IS NOT NULL THEN
548         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
549     END IF;
550
551     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
552     iscale := COALESCE(badge.importance_scale, 1.0);
553
554     RETURN QUERY
555      SELECT bib,
556             SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
557       FROM (
558          SELECT f.id AS bib,
559                 (1 + EXTRACT(EPOCH FROM AGE(h.fulfillment_time)) / iint)::INT AS hage,
560                 COUNT(h.id)::INT AS holds
561           FROM  action.hold_request h
562                 JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
563                 JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
564           WHERE h.fulfillment_time >= NOW() - badge.horizon_age
565                 AND h.request_lib = ANY (badge.orgs)
566           GROUP BY 1, 2
567       ) x
568       GROUP BY 1;
569 END;
570 $f$ LANGUAGE PLPGSQL STRICT;
571
572 CREATE OR REPLACE FUNCTION rating.holds_placed_over_time(badge_id INT)
573     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
574 DECLARE
575     badge   rating.badge_with_orgs%ROWTYPE;
576     iage    INT     := 1;
577     iint    INT     := NULL;
578     iscale  NUMERIC := NULL;
579 BEGIN
580
581     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
582
583     IF badge.horizon_age IS NULL THEN
584         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
585             badge.name,
586             badge.id;
587     END IF;
588
589     PERFORM rating.precalc_bibs_by_copy(badge_id);
590
591     SET LOCAL client_min_messages = error;
592     DROP TABLE IF EXISTS precalc_bib_list;
593     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
594         SELECT id FROM precalc_filter_bib_list
595             INTERSECT
596         SELECT id FROM precalc_bibs_by_copy_list;
597
598     iint := EXTRACT(EPOCH FROM badge.importance_interval);
599     IF badge.importance_age IS NOT NULL THEN
600         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
601     END IF;
602
603     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
604     iscale := COALESCE(badge.importance_scale, 1.0);
605
606     RETURN QUERY
607      SELECT bib,
608             SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
609       FROM (
610          SELECT f.id AS bib,
611                 (1 + EXTRACT(EPOCH FROM AGE(h.request_time)) / iint)::INT AS hage,
612                 COUNT(h.id)::INT AS holds
613           FROM  action.hold_request h
614                 JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
615                 JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
616           WHERE h.request_time >= NOW() - badge.horizon_age
617                 AND h.request_lib = ANY (badge.orgs)
618           GROUP BY 1, 2
619       ) x
620       GROUP BY 1;
621 END;
622 $f$ LANGUAGE PLPGSQL STRICT;
623
624 CREATE OR REPLACE FUNCTION rating.current_hold_count(badge_id INT)
625     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
626 DECLARE
627     badge   rating.badge_with_orgs%ROWTYPE;
628 BEGIN
629
630     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
631
632     PERFORM rating.precalc_bibs_by_copy(badge_id);
633
634     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
635         SELECT id FROM precalc_filter_bib_list
636             INTERSECT
637         SELECT id FROM precalc_bibs_by_copy_list
638     );
639
640     ANALYZE precalc_copy_filter_bib_list;
641
642     RETURN QUERY
643      SELECT rhrr.bib_record AS bib,
644             COUNT(DISTINCT h.id)::NUMERIC AS holds
645       FROM  action.hold_request h
646             JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
647             JOIN action.hold_copy_map m ON (m.hold = h.id)
648             JOIN precalc_copy_filter_bib_list cf ON (rhrr.bib_record = cf.id AND m.target_copy = cf.copy)
649       WHERE h.fulfillment_time IS NULL
650             AND h.request_lib = ANY (badge.orgs)
651       GROUP BY 1;
652 END;
653 $f$ LANGUAGE PLPGSQL STRICT;
654
655 CREATE OR REPLACE FUNCTION rating.circs_over_time(badge_id INT)
656     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
657 DECLARE
658     badge   rating.badge_with_orgs%ROWTYPE;
659     iage    INT     := 1;
660     iint    INT     := NULL;
661     iscale  NUMERIC := NULL;
662 BEGIN
663
664     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
665
666     IF badge.horizon_age IS NULL THEN
667         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
668             badge.name,
669             badge.id;
670     END IF;
671
672     PERFORM rating.precalc_bibs_by_copy(badge_id);
673
674     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
675         SELECT id FROM precalc_filter_bib_list
676             INTERSECT
677         SELECT id FROM precalc_bibs_by_copy_list
678     );
679
680     ANALYZE precalc_copy_filter_bib_list;
681
682     iint := EXTRACT(EPOCH FROM badge.importance_interval);
683     IF badge.importance_age IS NOT NULL THEN
684         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
685     END IF;
686
687     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
688     iscale := COALESCE(badge.importance_scale, 1.0);
689
690     RETURN QUERY
691      SELECT bib,
692             SUM( circs * GREATEST( iscale * (iage - cage), 1.0 ))
693       FROM (
694          SELECT cn.record AS bib,
695                 (1 + EXTRACT(EPOCH FROM AGE(c.xact_start)) / iint)::INT AS cage,
696                 COUNT(c.id)::INT AS circs
697           FROM  action.circulation c
698                 JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
699                 JOIN asset.copy cp ON (cp.id = c.target_copy)
700                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
701           WHERE c.xact_start >= NOW() - badge.horizon_age
702                 AND cn.owning_lib = ANY (badge.orgs)
703                 AND c.phone_renewal IS FALSE  -- we don't count renewals
704                 AND c.desk_renewal IS FALSE
705                 AND c.opac_renewal IS FALSE
706           GROUP BY 1, 2
707       ) x
708       GROUP BY 1;
709 END;
710 $f$ LANGUAGE PLPGSQL STRICT;
711
712 CREATE OR REPLACE FUNCTION rating.current_circ_count(badge_id INT)
713     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
714 DECLARE
715     badge   rating.badge_with_orgs%ROWTYPE;
716 BEGIN
717
718     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
719
720     PERFORM rating.precalc_bibs_by_copy(badge_id);
721
722     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
723         SELECT id FROM precalc_filter_bib_list
724             INTERSECT
725         SELECT id FROM precalc_bibs_by_copy_list
726     );
727
728     ANALYZE precalc_copy_filter_bib_list;
729
730     RETURN QUERY
731      SELECT cn.record AS bib,
732             COUNT(c.id)::NUMERIC AS circs
733       FROM  action.circulation c
734             JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
735             JOIN asset.copy cp ON (cp.id = c.target_copy)
736             JOIN asset.call_number cn ON (cn.id = cp.call_number)
737       WHERE c.checkin_time IS NULL
738             AND cn.owning_lib = ANY (badge.orgs)
739       GROUP BY 1;
740
741 END;
742 $f$ LANGUAGE PLPGSQL STRICT;
743
744 CREATE OR REPLACE FUNCTION rating.checked_out_total_ratio(badge_id INT)
745     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
746 DECLARE
747     badge   rating.badge_with_orgs%ROWTYPE;
748 BEGIN
749
750     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
751
752     PERFORM rating.precalc_bibs_by_copy(badge_id);
753
754     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
755         SELECT id FROM precalc_filter_bib_list
756             INTERSECT
757         SELECT id FROM precalc_bibs_by_copy_list
758     );
759
760     ANALYZE precalc_copy_filter_bib_list;
761
762     RETURN QUERY
763      SELECT bib,
764             SUM(checked_out)::NUMERIC / SUM(total)::NUMERIC
765       FROM  (SELECT cn.record AS bib,
766                     (cp.status = 1)::INT AS checked_out,
767                     1 AS total
768               FROM  asset.copy cp
769                     JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
770                     JOIN asset.call_number cn ON (cn.id = cp.call_number)
771               WHERE cn.owning_lib = ANY (badge.orgs)
772             ) x
773       GROUP BY 1;
774 END;
775 $f$ LANGUAGE PLPGSQL STRICT;
776
777 CREATE OR REPLACE FUNCTION rating.holds_total_ratio(badge_id INT)
778     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
779 DECLARE
780     badge   rating.badge_with_orgs%ROWTYPE;
781 BEGIN
782
783     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
784
785     PERFORM rating.precalc_bibs_by_copy(badge_id);
786
787     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
788         SELECT id FROM precalc_filter_bib_list
789             INTERSECT
790         SELECT id FROM precalc_bibs_by_copy_list
791     );
792
793     ANALYZE precalc_copy_filter_bib_list;
794
795     RETURN QUERY
796      SELECT cn.record AS bib,
797             COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
798       FROM  asset.copy cp
799             JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
800             JOIN asset.call_number cn ON (cn.id = cp.call_number)
801             JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
802       WHERE cn.owning_lib = ANY (badge.orgs)
803       GROUP BY 1;
804 END;
805 $f$ LANGUAGE PLPGSQL STRICT;
806
807 CREATE OR REPLACE FUNCTION rating.holds_holdable_ratio(badge_id INT)
808     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
809 DECLARE
810     badge   rating.badge_with_orgs%ROWTYPE;
811 BEGIN
812
813     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
814
815     PERFORM rating.precalc_bibs_by_copy(badge_id);
816
817     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
818         SELECT id FROM precalc_filter_bib_list
819             INTERSECT
820         SELECT id FROM precalc_bibs_by_copy_list
821     );
822
823     ANALYZE precalc_copy_filter_bib_list;
824
825     RETURN QUERY
826      SELECT cn.record AS bib,
827             COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
828       FROM  asset.copy cp
829             JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
830             JOIN asset.copy_location cl ON (cl.id = cp.location)
831             JOIN config.copy_status cs ON (cs.id = cp.status)
832             JOIN asset.call_number cn ON (cn.id = cp.call_number)
833             JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
834       WHERE cn.owning_lib = ANY (badge.orgs)
835             AND cp.holdable IS TRUE
836             AND cl.holdable IS TRUE
837             AND cs.holdable IS TRUE
838       GROUP BY 1;
839 END;
840 $f$ LANGUAGE PLPGSQL STRICT;
841
842 CREATE OR REPLACE FUNCTION rating.bib_record_age(badge_id INT)
843     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
844 DECLARE
845     badge   rating.badge_with_orgs%ROWTYPE;
846 BEGIN
847
848     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
849
850     PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
851
852     SET LOCAL client_min_messages = error;
853     DROP TABLE IF EXISTS precalc_bib_list;
854     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
855         SELECT id FROM precalc_filter_bib_list
856             INTERSECT
857         SELECT id FROM precalc_bibs_by_copy_or_uri_list;
858
859     RETURN QUERY
860      SELECT b.id,
861             1.0 / EXTRACT(EPOCH FROM AGE(b.create_date))::NUMERIC + 1.0
862       FROM  precalc_bib_list pop
863             JOIN biblio.record_entry b ON (b.id = pop.id);
864 END;
865 $f$ LANGUAGE PLPGSQL STRICT;
866
867 CREATE OR REPLACE FUNCTION rating.bib_pub_age(badge_id INT)
868     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
869 DECLARE
870     badge   rating.badge_with_orgs%ROWTYPE;
871 BEGIN
872
873     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
874
875     PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
876
877     SET LOCAL client_min_messages = error;
878     DROP TABLE IF EXISTS precalc_bib_list;
879     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
880         SELECT id FROM precalc_filter_bib_list
881             INTERSECT
882         SELECT id FROM precalc_bibs_by_copy_or_uri_list;
883
884     RETURN QUERY
885      SELECT pop.id AS bib,
886             s.value::NUMERIC
887       FROM  precalc_bib_list pop
888             JOIN metabib.record_sorter s ON (
889                 s.source = pop.id
890                 AND s.attr = 'pubdate'
891                 AND s.value ~ '^\d+$'
892             )
893       WHERE s.value::INT <= EXTRACT(YEAR FROM NOW())::INT;
894 END;
895 $f$ LANGUAGE PLPGSQL STRICT;
896
897 CREATE OR REPLACE FUNCTION rating.percent_time_circulating(badge_id INT)
898     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
899 DECLARE
900     badge   rating.badge_with_orgs%ROWTYPE;
901 BEGIN
902
903     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
904
905     PERFORM rating.precalc_bibs_by_copy(badge_id);
906
907     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
908         SELECT id FROM precalc_filter_bib_list
909             INTERSECT
910         SELECT id FROM precalc_bibs_by_copy_list
911     );
912
913     ANALYZE precalc_copy_filter_bib_list;
914
915     RETURN QUERY
916      SELECT bib,
917             SUM(COALESCE(circ_time,0))::NUMERIC / SUM(age)::NUMERIC
918       FROM  (SELECT cn.record AS bib,
919                     cp.id,
920                     EXTRACT( EPOCH FROM AGE(cp.active_date) ) + 1 AS age,
921                     SUM(  -- time copy spent circulating
922                         EXTRACT(
923                             EPOCH FROM
924                             AGE(
925                                 COALESCE(circ.checkin_time, circ.stop_fines_time, NOW()),
926                                 circ.xact_start
927                             )
928                         )
929                     )::NUMERIC AS circ_time
930               FROM  asset.copy cp
931                     JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
932                     JOIN asset.call_number cn ON (cn.id = cp.call_number)
933                     LEFT JOIN action.all_circulation circ ON (
934                         circ.target_copy = cp.id
935                         AND stop_fines NOT IN (
936                             'LOST',
937                             'LONGOVERDUE',
938                             'CLAIMSRETURNED',
939                             'LONGOVERDUE'
940                         )
941                         AND NOT (
942                             checkin_time IS NULL AND
943                             stop_fines = 'MAXFINES'
944                         )
945                     )
946               WHERE cn.owning_lib = ANY (badge.orgs)
947                     AND cp.active_date IS NOT NULL
948                     -- Next line requires that copies with no circs (circ.id IS NULL) also not be deleted
949                     AND ((circ.id IS NULL AND NOT cp.deleted) OR circ.id IS NOT NULL)
950               GROUP BY 1,2,3
951             ) x
952       GROUP BY 1;
953 END;
954 $f$ LANGUAGE PLPGSQL STRICT;
955
956 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy(badge_id INT)
957     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
958 BEGIN
959     PERFORM rating.precalc_bibs_by_copy(badge_id);
960     RETURN QUERY
961         SELECT id, 1.0 FROM precalc_filter_bib_list
962             INTERSECT
963         SELECT id, 1.0 FROM precalc_bibs_by_copy_list;
964 END;
965 $f$ LANGUAGE PLPGSQL STRICT;
966
967 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_uri(badge_id INT)
968     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
969 BEGIN
970     PERFORM rating.precalc_bibs_by_uri(badge_id);
971     RETURN QUERY
972         SELECT id, 1.0 FROM precalc_bib_filter_bib_list
973             INTERSECT
974         SELECT id, 1.0 FROM precalc_bibs_by_uri_list;
975 END;
976 $f$ LANGUAGE PLPGSQL STRICT;
977
978 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy_or_uri(badge_id INT)
979     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
980 BEGIN
981     PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
982     RETURN QUERY
983         (SELECT id, 1.0 FROM precalc_filter_bib_list
984             INTERSECT
985         SELECT id, 1.0 FROM precalc_bibs_by_copy_list)
986             UNION
987         (SELECT id, 1.0 FROM precalc_bib_filter_bib_list
988             INTERSECT
989         SELECT id, 1.0 FROM precalc_bibs_by_uri_list);
990 END;
991 $f$ LANGUAGE PLPGSQL STRICT;
992
993 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_global(badge_id INT)
994     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
995 BEGIN
996     RETURN QUERY
997         SELECT id, 1.0 FROM precalc_bib_filter_bib_list;
998 END;
999 $f$ LANGUAGE PLPGSQL STRICT;
1000
1001 CREATE INDEX hold_fulfillment_time_idx ON action.hold_request (fulfillment_time) WHERE fulfillment_time IS NOT NULL;
1002 CREATE INDEX hold_request_time_idx ON action.hold_request (request_time);
1003
1004 COMMIT;
1005